diff --git a/.github/workflows/tutorials-check.yml b/.github/workflows/tutorials-check.yml new file mode 100644 index 0000000..34ca3db --- /dev/null +++ b/.github/workflows/tutorials-check.yml @@ -0,0 +1,73 @@ +name: Standalone tutorial checks + +on: + pull_request: + branches: [starter, final] + push: + branches: [starter, final] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: tutorials-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tutorials: + name: ${{ matrix.app }} / Node ${{ matrix.node }} + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + node: ['22', '24'] + app: + - account-sharing + - account-takeover + - ban-evasion + - bonus-abuse + - card-testing + - chargeback-dispute + - checkout-risk + - coupon-abuse + - credential-stuffing + - loan-risk + - new-account-fraud + - paywall + - personalization + - promo-abuse + - referral-fraud + - regional-pricing + - sms-pumping + - survey-fraud + - sybil-attack + - web-scraping + env: + PUPPETEER_SKIP_DOWNLOAD: 'true' + DOTENV_CONFIG_PATH: /tmp/shieldlabs-tutorial-ci-no-env + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: ${{ matrix.node }} + cache: npm + cache-dependency-path: ${{ matrix.app }}/package-lock.json + - name: Check folder independence, lockfile, placeholders and syntax + run: node scripts/check-tutorial.mjs "${{ matrix.app }}" + - name: Install the ordinary user's dependencies + working-directory: ${{ matrix.app }} + run: npm ci --omit=dev + - name: Check the app + working-directory: ${{ matrix.app }} + run: npm run check + - name: Native tests with synthetic History only + working-directory: ${{ matrix.app }} + run: npm test + - name: Audit runtime dependencies + working-directory: ${{ matrix.app }} + run: npm audit --omit=dev --audit-level=high diff --git a/.gitignore b/.gitignore index da0c9a6..de22757 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules/ .env *.sqlite *.sqlite-journal +*.sqlite-wal +*.sqlite-shm .DS_Store diff --git a/README.md b/README.md index dabb966..ae675d2 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,69 @@ # ShieldLabs use case tutorials -This repository contains runnable tutorials that show how to use [ShieldLabs](https://shieldlabs.ai) to detect and stop common fraud and abuse: fake signups, account takeover, coupon and paywall abuse, card testing, SMS pumping, scraping and more. - -ShieldLabs is a fraud detection and prevention API. A JavaScript snippet identifies each visit in the browser, and your backend reads the result of that identification: the Device ID, the Visitor ID, a Risk Score from 0 to 100 and the named risk signals behind it. - -Each tutorial is a self-contained demo application with a **starter** branch (the app without protection) and a **final** branch (the completed ShieldLabs integration). The difference between the two branches is exactly what the integration adds: - -```bash -git diff starter final -- new-account-fraud +Twenty independent reference applications for visitor identification and abuse prevention. Every folder contains its own browser page, Fastify server, SQLite database, dependencies and tests. Copy one folder to use it; no other scenario or shared decision-lab server is required. + +## Two versions + +- `starter`: the app before integrating ShieldLabs. It works locally without keys. +- `final`: the same app with pinned browser and Node SDKs and a server-side decision. + +Compare the integration with `git diff starter final -- new-account-fraud`. Stop the server before switching versions and run `npm ci --omit=dev` again in the chosen app. Do not discard your own local changes just to switch branches; use another clone if necessary. The public default branch is `starter`; switch to `final` for the completed ShieldLabs integration. + +## Applications + +- [New account fraud](./new-account-fraud/README.md): One trial per Device ID. +- [Checkout review](./checkout-risk/README.md): Dangerous orders go to review. +- [Paywall enforcement](./paywall/README.md): Two distinct free articles per device per day. +- [Account sharing](./account-sharing/README.md): One active device per demo account. +- [Account takeover](./account-takeover/README.md): A new device needs another factor. +- [Ban evasion](./ban-evasion/README.md): A new account does not bypass a device ban. +- [Welcome bonus](./bonus-abuse/README.md): One welcome match per device. +- [Card testing](./card-testing/README.md): Declined attempts are counted per device. +- [Chargeback evidence](./chargeback-dispute/README.md): Store the verified device behind an order. +- [Coupon abuse](./coupon-abuse/README.md): One use of a code per device, with a code cooldown. +- [Credential stuffing](./credential-stuffing/README.md): Failed passwords are counted per device. +- [Loan application review](./loan-risk/README.md): Conflicting same-device applications need review. +- [Returning visitor personalization](./personalization/README.md): Preferences follow a verified device rather than only its session cookie. +- [First-order promotion](./promo-abuse/README.md): One first-order discount per device. +- [Referral fraud](./referral-fraud/README.md): Same-device referrals earn no reward. +- [Regional pricing](./regional-pricing/README.md): A regional price needs a matching unmasked connection. +- [SMS pumping](./sms-pumping/README.md): Three synthetic sends per device per day, with growing waits. +- [Survey fraud](./survey-fraud/README.md): One reward-bearing survey per device. +- [Sybil claims](./sybil-attack/README.md): One simulated airdrop claim per device. +- [Web scraping](./web-scraping/README.md): Unverified or automated searches receive no flight prices. + +## Run one app + +Use Node.js 22 or later. From the repository root: + +```sh +git switch starter +cd new-account-fraud +npm ci --omit=dev +cp .env.example .env +npm run dev ``` -## Tutorials - -- [**Account sharing prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/account-sharing): keep one device signed in per account and let the owner sign the other device out. -- [**Account takeover prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/account-takeover): step up a sign-in that brings the right password from a device the account has never used. -- [**Ban evasion prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/ban-evasion): ban every device a banned user signed in from, so a fresh account does not get them back in. -- [**Bonus abuse prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/bonus-abuse): pay a welcome deposit-match bonus once per device, no matter how many accounts try to claim it. -- [**Card testing prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/card-testing): cap declined card attempts per device and refuse automated checkouts. -- [**Chargeback dispute evidence**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/chargeback-dispute): store the device behind every order and assemble evidence against friendly-fraud chargebacks. -- [**Coupon abuse prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/coupon-abuse): allow one redemption per code per device, with a cooldown before a second code. -- [**Promo abuse prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/promo-abuse): apply an automatic first-order discount once per device, not once per email address. -- [**Referral fraud prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/referral-fraud): pay a referral reward only when the referrer and the new signup are on different devices. -- [**Credential stuffing prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/credential-stuffing): limit failed sign-ins per device, refuse automation and challenge sign-ins from unknown devices. -- [**Loan application fraud prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/loan-risk): flag applications whose name or income changes between attempts from the same device. -- [**New account fraud prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/new-account-fraud): allow one free trial per device and refuse automated or Dangerous signups. -- [**Paywall enforcement**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/paywall): meter free articles per device, so incognito windows and cleared cookies do not reset the count. -- [**Returning visitor personalization**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/personalization): keep search history and saved items for a returning device, even in incognito or after cookies are cleared. -- [**Regional pricing enforcement**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/regional-pricing): apply a regional discount only for the country of an unmasked connection. -- [**SMS pumping prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/sms-pumping): cap verification codes per device with a growing wait, and refuse automation and Tor. -- [**Sybil attack prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/sybil-attack): accept one airdrop claim per device, no matter how many wallet addresses try. -- [**Survey fraud prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/survey-fraud): accept one paid survey submission per device. -- [**Web scraping prevention**](https://github.com/ShieldLabs-ai/use-case-tutorials/tree/starter/web-scraping): serve flight prices to real browsers and refuse automated requests. - -## General setup - -You need Node.js 20 or later. - -1. Clone this repo and, **within the folder of the use case you want to try**, install the dependencies: - - ```bash - npm install - ``` - - This also downloads a Chrome build for the Puppeteer bot test. - -2. Copy `.env.example` to `.env`. On the final branch, add your ShieldLabs keys (see below). - -3. Start the server: - - ```bash - npm run dev - ``` +Open http://127.0.0.1:3000. Replace the folder name to choose another app. To use the optional headless bot script, install dev dependencies with `npm ci`. -4. Open [http://localhost:3000](http://localhost:3000) for the starter branch, or your development domain for the final branch. +## Run final -## The domain requirement +Stop the starter server, switch to `final`, reinstall this app's dependencies and copy the final .env.example settings into .env. Use the Public Key and Private API Key for a hostname registered in **Integration > Domains**, copied from **Integration > API keys**. Serve the app through HTTPS on that hostname via your existing deployment or reverse proxy. The API does not automatically accept a customer's key on localhost. No particular hosting provider is required. -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account. The Public Key is bound to that domain, so pages served from `localhost` or a raw IP address are rejected. The starter branch runs on [http://localhost:3000](http://localhost:3000) as is. To run the final branch: +`public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh request ID for an action. `server/shieldlabs.js` uses `@shieldlabs-ai/node@1.0.1` to read History and evaluate the identification. It rereads at least 11 seconds after the observation as a demo precaution, not a guaranteed finality marker. Failed, stale and reused checks do not authorize actions. Most examples refuse Dangerous traffic; checkout-risk sends a verified Dangerous order for review. Automation is refused in both. Read the code of the selected scenario for its own limits. -1. [Start Free](https://app.shieldlabs.ai/) (5,000 identifications one time, no credit card) or sign in to the dashboard. -2. Register a development domain, for example `tutorial.your-domain.com`, as its own domain under **Integration > Domains**, rather than reusing the keys of your production domain. The hostname must resolve in public DNS. The first identification from the domain verifies it. If you serve the app on a subdomain of the domain you registered, keep its subdomains **Accepted** (the default). On the Free and Starter plans your account holds one domain, so the development domain takes that slot. See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains). -3. Open **Integration > API keys** for that domain and copy its keys into `.env`: the **Public Key** as `SHIELDLABS_PUBLIC_KEY` (it goes to the browser) and the **Private API Key** (`sec_...`) as `SHIELDLABS_API_KEY` (it stays on the server). -4. Tunnel the domain to your local server. With [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), for a domain whose DNS is on Cloudflare: +## Verify and reset - ```bash - cloudflared tunnel login - cloudflared tunnel create shieldlabs-tutorial - cloudflared tunnel route dns shieldlabs-tutorial tutorial.your-domain.com - cloudflared tunnel run --url http://localhost:3000 shieldlabs-tutorial - ``` - - Any tunnel or reverse proxy that serves the app on your registered hostname works. - -5. Run `npm run dev` and open `https://tutorial.your-domain.com`. - -## How the final apps use ShieldLabs - -- **In the browser**, `public/shieldlabs.js` loads the snippet from `cdn.shieldlabs.ai` with the Public Key served by `/config.js`. When the user starts an action (the first focus of the form), it runs `forceCheckAnonymous` and waits for the request ID of that identification, which the page sends to the server with the form. The browser never sees a Risk Score. -- **On the server**, `server/shieldlabs.js` reads the identification from the [History API](https://docs.shieldlabs.ai/api/server-api) with the Private API Key. Scoring is asynchronous, so it polls every half second for up to about 10 seconds. -- **Every protected handler starts with the same guard**: no identification is refused as unverified (never treated as clean), the 999 rate-limit marker and the all-zero Device ID are refused, an identification older than 5 minutes or already used for another action is refused, and browser automation, disabled JavaScript or a Risk Score in the Dangerous band is refused. Risk Score bands: **Trusted** 0-29, **Suspicious** 30-59, **Dangerous** 60-100. -- **The use case rule keys on the Device ID**, which holds through cleared cookies, incognito windows and IP changes. The Visitor ID changes when cookies are cleared, so the tutorials do not key on it. - -The refusal messages in the demos name the reason to make the tutorials easy to follow. In production, show a generic message and log the reason. - -## Run the bot test - -Each app has a `test-bot.js` Puppeteer script that runs the main flow in headless Chrome and prints the server's answer. On the starter branch the bot gets through: - -```bash -node test-bot.js -``` - -On the final branch, point it at your development domain. Headless Chrome raises the Browser Automation signal, so the server refuses the action: - -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js -``` +GitHub Actions checks every standalone folder on Node 22 and 24: folder independence, lockfile consistency, placeholder-only environment examples, JavaScript syntax, clean runtime installation, native tests and dependency audit. It uses no real account keys and does not run live scoring or the optional bot scripts. From the repository root, run `node scripts/check-tutorial.mjs` to check all folders or pass one folder name. -## Resetting the demo databases +Run `npm run check` and `npm test` in each folder. Automated tests use isolated SQLite and synthetic History responses, not live scoring. `DEMO_ALLOW_RESET=1` enables the disposable reset control and `npm run reset-db` clears that app's teaching state. A schema change when switching starter/final may recreate the demo database. Never put important data there. -Each app keeps its data in a local SQLite file, `db.sqlite`. To reset it: +Live checks need your registered hostname and matching real History rows. Space identifications over a minute apart, avoid parallel checks and repeated cookie resets, and stop on rate limiting. Use only invented personal details and supplied sample cards. Deposits, purchases, reward credits, SMS, challenges and loan decisions are simulated. -- Click **Reset demo DB** at the bottom of the demo app page, or -- Run this from the app folder: +## Production boundary - ```bash - npm run reset-db - ``` +The examples are not drop-in production security modules. Demonstration passwords, moderation/reset controls and admin evidence screens are intentionally for a disposable demo. A real application needs its own authentication, authorization, session/action binding, durable state and a shared atomic replay store. The example device limits are not ShieldLabs High-Risk Event thresholds. Repository publication and green synthetic CI are not proof that every live scoring scenario has passed. Verify the completed app with your own registered hostname before adapting it. ## License -[MIT](LICENSE) +[MIT](./LICENSE) diff --git a/account-sharing/.env.example b/account-sharing/.env.example index f16d12c..b5a6d76 100644 --- a/account-sharing/.env.example +++ b/account-sharing/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/account-sharing/LICENSE b/account-sharing/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/account-sharing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/account-sharing/README.md b/account-sharing/README.md index 695efd0..3725cd2 100644 --- a/account-sharing/README.md +++ b/account-sharing/README.md @@ -1,66 +1,44 @@ -# ShieldLabs Account Sharing Tutorial +# ShieldLabs Account sharing tutorial -This tutorial shows how to enforce a one-device-at-a-time plan with ShieldLabs: a sign-in from a different device than the one already signed in is refused until the owner signs that device out. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **One active device per demo account**. -See the full guide at [Account Sharing](https://docs.shieldlabs.ai/use-case/account-sharing). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- account-sharing`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- account-sharing`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the sign-in form. The request ID of that identification goes to the server with the credentials. -- `server/shieldlabs.js` reads the identification from the History API. The sign-in is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/accounts.js` stores the Device ID with each session. A sign-in from a different device than the active session's is refused, unless the user chooses **Sign out the other device and sign in here**. The same device signing in again, after clearing cookies or in an incognito window, keeps working. -- The signed-in page checks its session every 5 seconds and says so when another device signed it out. It also calls `checkAuthenticatedUser` with a hashed account id, which ties the visit to the account for the High-Risk Events ShieldLabs detects on users, such as Account sharing. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Sign in with the demo account `demo@example.com` / `demo-password`. -2. Open an incognito window in the same browser and sign in again. It works without a prompt: the Device ID is the same, so this is still one device, and the new session replaces the old one. -3. Open the app in a different browser, or on your phone, and sign in. It is refused: the account is signed in on another device. -4. Select **Sign out the other device and sign in here**. Within a few seconds the first browser shows that it was signed out on another device. +Sign in with demo@example.com / demo-password. On a separately observed second device, sign-in asks whether to sign out the other device. Confirming replaces the old session. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, sign in from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The sign-in is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Account sharing](https://docs.shieldlabs.ai/tutorials/account-sharing) (the new guide stays local until its separate documentation release). diff --git a/account-sharing/package-lock.json b/account-sharing/package-lock.json index 7b7cadf..6547870 100644 --- a/account-sharing/package-lock.json +++ b/account-sharing/package-lock.json @@ -11,38 +11,17 @@ "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -291,76 +270,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -369,15 +322,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -428,47 +372,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -498,20 +426,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -521,86 +435,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -620,15 +454,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -695,124 +520,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -837,20 +597,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -877,10 +623,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -894,10 +641,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -907,29 +655,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -939,67 +670,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1008,26 +678,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1040,12 +690,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1153,15 +797,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1191,39 +826,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1265,32 +885,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1310,22 +904,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1336,64 +914,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1469,19 +989,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1543,33 +1061,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1598,68 +1112,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1685,18 +1137,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1776,40 +1216,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1820,42 +1226,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1900,15 +1307,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1918,15 +1316,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2083,44 +1472,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2130,16 +1481,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2157,17 +1498,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2177,29 +1507,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2236,24 +1573,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2289,12 +1608,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2307,17 +1620,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2325,28 +1632,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2357,6 +1684,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2378,45 +1706,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2424,6 +1744,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/account-sharing/package.json b/account-sharing/package.json index 38b20c2..83b6a97 100644 --- a/account-sharing/package.json +++ b/account-sharing/package.json @@ -7,21 +7,25 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/account-sharing/public/index.html b/account-sharing/public/index.html index 1b6a5c7..486b131 100644 --- a/account-sharing/public/index.html +++ b/account-sharing/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Stream

@@ -23,8 +24,7 @@

Sign in to watch

type="email" autocomplete="username" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" value="demo@example.com" />
+ diff --git a/account-sharing/public/shieldlabs.js b/account-sharing/public/shieldlabs.js index 5842a2e..6be835e 100644 --- a/account-sharing/public/shieldlabs.js +++ b/account-sharing/public/shieldlabs.js @@ -1,62 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; } -// On signed-in pages: ties the visit to the account with its hashed id, never a raw -// email. Account-level risk and the High-Risk Events ShieldLabs detects on users, -// such as Account sharing, build on it. -export async function checkSignedInUser(hashedUserId) { - const shieldlabs = await snippet; - shieldlabs?.checkAuthenticatedUser(hashedUserId); +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/account-sharing/server/db.js b/account-sharing/server/db.js index b642242..0f3b70f 100644 --- a/account-sharing/server/db.js +++ b/account-sharing/server/db.js @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { hashPassword } from './passwords.js'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/account-sharing/server/server.js b/account-sharing/server/server.js index c182e14..c4cb068 100644 --- a/account-sharing/server/server.js +++ b/account-sharing/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyCookie from '@fastify/cookie'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; @@ -8,14 +12,18 @@ import { getSession, signIn, signOut } from './accounts.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyCookie); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Sign in and store the session token in a cookie. @@ -23,7 +31,7 @@ app.post('/api/login', async (request, reply) => { const { email, password, requestId, signOutOtherDevice } = request.body ?? {}; const result = await signIn({ email, password, requestId, signOutOtherDevice: signOutOtherDevice === true }); if (result.success) { - reply.setCookie('session', result.token, { httpOnly: true, sameSite: 'lax', path: '/' }); + reply.setCookie('shieldlabs_demo_account_sharing_session', result.token, { httpOnly: true, sameSite: 'lax', path: '/' }); } return reply.send({ success: result.success, otherDevice: result.otherDevice, message: result.message }); }); @@ -35,28 +43,33 @@ app.get('/api/session', async (request, reply) => { app.post('/api/logout', async (request, reply) => { signOut(sessionToken(request)); - reply.clearCookie('session', { path: '/' }); + reply.clearCookie('shieldlabs_demo_account_sharing_session', { path: '/' }); return reply.send({ success: true, message: 'Signed out.' }); }); // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} // Reads the session cookie without a cookie-parsing dependency. function sessionToken(request) { - const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)session=([a-f0-9]+)/); + const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)shieldlabs_demo_account_sharing_session=([a-f0-9]+)/); return match ? match[1] : null; } diff --git a/account-sharing/server/shieldlabs.js b/account-sharing/server/shieldlabs.js index 791eb66..e577332 100644 --- a/account-sharing/server/shieldlabs.js +++ b/account-sharing/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/account-sharing/test/app.test.js b/account-sharing/test/app.test.js new file mode 100644 index 0000000..ad68553 --- /dev/null +++ b/account-sharing/test/app.test.js @@ -0,0 +1,141 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/login",{"email":"demo@example.com","password":"demo-password"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('account-sharing: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('a second device needs permission to replace the active session', async () => { + const first = await act(); assert.equal(first.body.success, true); + const second = await act({}, { device: DEVICE_B }); assert.equal(second.body.otherDevice, true); + const third = await act({ signOutOtherDevice: true }, { device: DEVICE_B }); assert.equal(third.body.success, true); + const session = await app.inject({ url: '/api/session', headers: { cookie: first.cookie } }); + assert.equal(session.json().signedOutElsewhere, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/account-sharing/test/browser.test.js b/account-sharing/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/account-sharing/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/account-takeover/.env.example b/account-takeover/.env.example index f16d12c..b5a6d76 100644 --- a/account-takeover/.env.example +++ b/account-takeover/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/account-takeover/LICENSE b/account-takeover/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/account-takeover/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/account-takeover/README.md b/account-takeover/README.md index 9500cd3..c9924b4 100644 --- a/account-takeover/README.md +++ b/account-takeover/README.md @@ -1,65 +1,44 @@ -# ShieldLabs Account Takeover Tutorial +# ShieldLabs Account takeover tutorial -This tutorial shows how to stop account takeover with ShieldLabs: a correct password from a device the account has never used must pass a step-up check, and automated or Dangerous sign-ins are refused. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **A new device needs another factor**. -See the full guide at [Account Takeover](https://docs.shieldlabs.ai/use-case/account-takeover). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- account-takeover`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- account-takeover`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the sign-in form. The request ID of that identification goes to the server with the credentials. -- `server/shieldlabs.js` reads the identification from the History API. The sign-in is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/accounts.js` keeps the known devices of each account, keyed by Device ID. A known device signs in with the password. A correct password from an unknown device gets a one-time code by email first (simulated: the code is shown on the page), and the device becomes known once the code is entered. In the demo, the first device an account ever signs in from becomes its first known device. -- The signed-in page calls `checkAuthenticatedUser` with a hashed account id, which ties the visit to the account for the High-Risk Events ShieldLabs detects on users, such as Account takeover. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Sign in with the demo account `demo@example.com` / `demo-password`. This is the account owner, and this browser becomes the first known device. -2. Sign out, then sign in from a different browser with the same password, as someone with a leaked password would. The app asks for a one-time code instead of letting them in. -3. Enter the code shown on the page. The new device is added to **Known devices**, and later sign-ins from it need only the password. +Sign in with demo@example.com / demo-password. From another observed device, enter the demo code shown on the page to finish the challenge. No email is sent. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, sign in from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The sign-in is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Account takeover](https://docs.shieldlabs.ai/tutorials/new-login-device) (the new guide stays local until its separate documentation release). diff --git a/account-takeover/package-lock.json b/account-takeover/package-lock.json index 019f959..3b0954e 100644 --- a/account-takeover/package-lock.json +++ b/account-takeover/package-lock.json @@ -11,38 +11,17 @@ "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -291,76 +270,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -369,15 +322,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -428,47 +372,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -498,20 +426,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -521,86 +435,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -620,15 +454,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -695,124 +520,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -837,20 +597,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -877,10 +623,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -894,10 +641,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -907,29 +655,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -939,67 +670,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1008,26 +678,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1040,12 +690,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1153,15 +797,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1191,39 +826,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1265,32 +885,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1310,22 +904,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1336,64 +914,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1469,19 +989,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1543,33 +1061,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1598,68 +1112,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1685,18 +1137,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1776,40 +1216,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1820,42 +1226,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1900,15 +1307,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1918,15 +1316,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2083,44 +1472,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2130,16 +1481,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2157,17 +1498,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2177,29 +1507,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2236,24 +1573,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2289,12 +1608,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2307,17 +1620,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2325,28 +1632,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2357,6 +1684,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2378,45 +1706,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2424,6 +1744,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/account-takeover/package.json b/account-takeover/package.json index bcdff2f..c1d0322 100644 --- a/account-takeover/package.json +++ b/account-takeover/package.json @@ -7,21 +7,25 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/account-takeover/public/index.html b/account-takeover/public/index.html index 044973a..21199ba 100644 --- a/account-takeover/public/index.html +++ b/account-takeover/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Wallet

@@ -22,8 +23,7 @@

Sign in to your wallet

type="email" autocomplete="username" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" value="demo@example.com" />
+ diff --git a/account-takeover/public/index.js b/account-takeover/public/index.js index fcde9c2..8e89dfc 100644 --- a/account-takeover/public/index.js +++ b/account-takeover/public/index.js @@ -89,6 +89,8 @@ function showCodeForm(stepUp) { challengeId = stepUp.challengeId; document.getElementById('demoCode').textContent = stepUp.demoCode; codeForm.reset(); + // Demo-only: the simulator returns this code openly; no real email is sent. + codeForm.elements.code.value = stepUp.demoCode; } codeForm.classList.toggle('hidden', !stepUp); } diff --git a/account-takeover/public/shieldlabs.js b/account-takeover/public/shieldlabs.js index 9aefc15..6be835e 100644 --- a/account-takeover/public/shieldlabs.js +++ b/account-takeover/public/shieldlabs.js @@ -1,62 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; } -// On signed-in pages: ties the visit to the account with its hashed id, never a raw -// email. Account-level risk and the High-Risk Events ShieldLabs detects on users, -// such as Account takeover, build on it. -export async function checkSignedInUser(hashedUserId) { - const shieldlabs = await snippet; - shieldlabs?.checkAuthenticatedUser(hashedUserId); +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/account-takeover/server/db.js b/account-takeover/server/db.js index 38e3573..d677b97 100644 --- a/account-takeover/server/db.js +++ b/account-takeover/server/db.js @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { hashPassword } from './passwords.js'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/account-takeover/server/server.js b/account-takeover/server/server.js index fb1b4bc..107f378 100644 --- a/account-takeover/server/server.js +++ b/account-takeover/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyCookie from '@fastify/cookie'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; @@ -8,21 +12,25 @@ import { getAccount, signIn, signOut, verifyCode } from './accounts.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyCookie); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Sign in and store the session token in a cookie. app.post('/api/login', async (request, reply) => { const { email, password, requestId } = request.body ?? {}; const { token, ...result } = await signIn({ email, password, requestId }); - if (token) reply.setCookie('session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); + if (token) reply.setCookie('shieldlabs_demo_account_takeover_session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); return reply.send(result); }); @@ -30,7 +38,7 @@ app.post('/api/login', async (request, reply) => { app.post('/api/verify-code', async (request, reply) => { const { challengeId, code } = request.body ?? {}; const { token, ...result } = await verifyCode({ challengeId, code }); - if (token) reply.setCookie('session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); + if (token) reply.setCookie('shieldlabs_demo_account_takeover_session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); return reply.send(result); }); @@ -41,28 +49,33 @@ app.get('/api/me', async (request, reply) => { app.post('/api/logout', async (request, reply) => { signOut(sessionToken(request)); - reply.clearCookie('session', { path: '/' }); + reply.clearCookie('shieldlabs_demo_account_takeover_session', { path: '/' }); return reply.send({ success: true, message: 'Signed out.' }); }); // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} // Reads the session cookie without a cookie-parsing dependency. function sessionToken(request) { - const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)session=([a-f0-9]+)/); + const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)shieldlabs_demo_account_takeover_session=([a-f0-9]+)/); return match ? match[1] : null; } diff --git a/account-takeover/server/shieldlabs.js b/account-takeover/server/shieldlabs.js index 791eb66..e577332 100644 --- a/account-takeover/server/shieldlabs.js +++ b/account-takeover/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/account-takeover/test/app.test.js b/account-takeover/test/app.test.js new file mode 100644 index 0000000..0416a1e --- /dev/null +++ b/account-takeover/test/app.test.js @@ -0,0 +1,140 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/login",{"email":"demo@example.com","password":"demo-password"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('account-takeover: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('a new device must complete the synthetic second factor', async () => { + assert.equal((await act()).body.success, true); + const challenge = await act({}, { device: DEVICE_B }); assert.equal(challenge.body.stepUp, true); + const verify = await app.inject({ method: 'POST', url: '/api/verify-code', payload: { challengeId: challenge.body.challengeId, code: challenge.body.demoCode } }); + assert.equal(verify.json().success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/account-takeover/test/browser.test.js b/account-takeover/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/account-takeover/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/ban-evasion/.env.example b/ban-evasion/.env.example index f16d12c..b5a6d76 100644 --- a/ban-evasion/.env.example +++ b/ban-evasion/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/ban-evasion/LICENSE b/ban-evasion/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/ban-evasion/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ban-evasion/README.md b/ban-evasion/README.md index aff7581..0049e36 100644 --- a/ban-evasion/README.md +++ b/ban-evasion/README.md @@ -1,64 +1,44 @@ -# ShieldLabs Ban Evasion Tutorial +# ShieldLabs Ban evasion tutorial -This tutorial shows how to make bans stick with ShieldLabs: banning a member also bans every device they signed in from, so a fresh account on the same device does not get them back in. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **A new account does not bypass a device ban**. -See the full guide at [Ban Evasion](https://docs.shieldlabs.ai/use-case/ban-evasion). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- ban-evasion`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- ban-evasion`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the account form. The request ID of that identification goes to the server with the signup or the sign-in. -- `server/shieldlabs.js` reads the identification from the History API. The signup or sign-in is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/forum.js` records the Device ID of every signup and sign-in for each member. Banning a member bans all of those devices too, and a new account or a sign-in from a banned device is refused. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Sign up as a new member, for example `trouble_1`, and write a post. -2. In **Moderator tools**, ban `trouble_1`. The ban covers the account and the device it used. -3. Clear the site's cookies, or open an incognito window, and sign up as `trouble_2`. It is refused: the Device ID is the same, and the device is banned. +Create a synthetic forum user, ban that user with the disposable moderator action, then attempt a fresh username from the same device. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, create an account from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The signup is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Ban evasion](https://docs.shieldlabs.ai/tutorials/ban-evasion) (the new guide stays local until its separate documentation release). diff --git a/ban-evasion/package-lock.json b/ban-evasion/package-lock.json index 4e6fb7d..7aa92e6 100644 --- a/ban-evasion/package-lock.json +++ b/ban-evasion/package-lock.json @@ -11,38 +11,17 @@ "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -291,76 +270,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -369,15 +322,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -428,47 +372,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -498,20 +426,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -521,86 +435,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -620,15 +454,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -695,124 +520,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -837,20 +597,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -877,10 +623,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -894,10 +641,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -907,29 +655,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -939,67 +670,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1008,26 +678,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1040,12 +690,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1153,15 +797,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1191,39 +826,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1265,32 +885,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1310,22 +904,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1336,64 +914,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1469,19 +989,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1543,33 +1061,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1598,68 +1112,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1685,18 +1137,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1776,40 +1216,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1820,42 +1226,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1900,15 +1307,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1918,15 +1316,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2083,44 +1472,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2130,16 +1481,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2157,17 +1498,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2177,29 +1507,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2236,24 +1573,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2289,12 +1608,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2307,17 +1620,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2325,28 +1632,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2357,6 +1684,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2378,45 +1706,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2424,6 +1744,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/ban-evasion/package.json b/ban-evasion/package.json index d2bd124..9ee4ce0 100644 --- a/ban-evasion/package.json +++ b/ban-evasion/package.json @@ -7,21 +7,25 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/ban-evasion/public/index.html b/ban-evasion/public/index.html index 211f663..53894e5 100644 --- a/ban-evasion/public/index.html +++ b/ban-evasion/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Forum

Neighborhood board

@@ -40,8 +41,7 @@

Your account

placeholder="Username" autocomplete="username" required - class="w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="w-full rounded-md border border-slate-300 px-3 py-2" value="tutorial_one" /> Your account placeholder="Password" autocomplete="current-password" required - class="w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="w-full rounded-md border border-slate-300 px-3 py-2" value="synthetic-password" />
+ diff --git a/ban-evasion/public/shieldlabs.js b/ban-evasion/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/ban-evasion/public/shieldlabs.js +++ b/ban-evasion/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/ban-evasion/server/db.js b/ban-evasion/server/db.js index 9e2b7c4..7a734e7 100644 --- a/ban-evasion/server/db.js +++ b/ban-evasion/server/db.js @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import { hashPassword } from './passwords.js'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/ban-evasion/server/server.js b/ban-evasion/server/server.js index bf91e80..bc8a7f5 100644 --- a/ban-evasion/server/server.js +++ b/ban-evasion/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyCookie from '@fastify/cookie'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; @@ -8,14 +12,18 @@ import { banUser, createPost, getBoard, signIn, signOut, signUp } from './forum. initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyCookie); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // The board: posts, members and the signed-in member. @@ -36,7 +44,7 @@ app.post('/api/signin', async (request, reply) => { app.post('/api/signout', async (request, reply) => { signOut(sessionToken(request)); - reply.clearCookie('session', { path: '/' }); + reply.clearCookie('shieldlabs_demo_ban_evasion_session', { path: '/' }); return reply.send({ success: true, message: 'Signed out.' }); }); @@ -51,29 +59,34 @@ app.post('/api/members/:username/ban', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} // --- Helpers --- function sendWithSession(reply, { token, ...result }) { - if (token) reply.setCookie('session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); + if (token) reply.setCookie('shieldlabs_demo_ban_evasion_session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); return reply.send(result); } // Reads the session cookie without a cookie-parsing dependency. function sessionToken(request) { - const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)session=([a-f0-9]+)/); + const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)shieldlabs_demo_ban_evasion_session=([a-f0-9]+)/); return match ? match[1] : null; } diff --git a/ban-evasion/server/shieldlabs.js b/ban-evasion/server/shieldlabs.js index 791eb66..e577332 100644 --- a/ban-evasion/server/shieldlabs.js +++ b/ban-evasion/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/ban-evasion/test/app.test.js b/ban-evasion/test/app.test.js new file mode 100644 index 0000000..a4e18c4 --- /dev/null +++ b/ban-evasion/test/app.test.js @@ -0,0 +1,140 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/signup",{"username":"tutorial_one","password":"synthetic-password"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('ban-evasion: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('a new account does not bypass a device ban', async () => { + assert.equal((await act()).body.success, true); + assert.equal((await app.inject({ method: 'POST', url: '/api/members/tutorial_one/ban' })).json().success, true); + assert.equal((await act({ username: 'tutorial_two' })).body.success, false); + assert.equal((await act({ username: 'tutorial_two' }, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/ban-evasion/test/browser.test.js b/ban-evasion/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/ban-evasion/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/bonus-abuse/.env.example b/bonus-abuse/.env.example index f16d12c..b5a6d76 100644 --- a/bonus-abuse/.env.example +++ b/bonus-abuse/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/bonus-abuse/LICENSE b/bonus-abuse/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/bonus-abuse/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bonus-abuse/README.md b/bonus-abuse/README.md index b8ad1d7..80dd51b 100644 --- a/bonus-abuse/README.md +++ b/bonus-abuse/README.md @@ -1,65 +1,44 @@ -# ShieldLabs Bonus Abuse Tutorial +# ShieldLabs Welcome bonus tutorial -This tutorial shows how to stop bonus abuse with ShieldLabs: a device gets the welcome deposit match once, no matter how many accounts sign up to claim it again, and automated or Dangerous signups and deposits are refused. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **One welcome match per device**. -See the full guide at [Bonus Abuse](https://docs.shieldlabs.ai/use-case/bonus-abuse). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- bonus-abuse`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- bonus-abuse`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the signup form or the deposit form. The request ID of that identification goes to the server with the request. -- `server/shieldlabs.js` reads the identification from the History API. The signup or deposit is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/wallet.js` credits the 100% welcome match, up to $100, on the first deposit made from a device, keyed by Device ID rather than by account: opening a new account from the same device does not unlock a second match. -- The signed-in page calls `checkAuthenticatedUser` with a hashed account id, which ties the visit to the account for the High-Risk Events ShieldLabs detects on users, such as Multi-accounting. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Create an account and make a deposit, for example $50. A $50 welcome bonus is credited (100% match, up to $100). -2. Sign out, create a second account from the same browser, and deposit again, as someone opening account after account would. No match this time: the Device ID already claimed one, even though the account is brand new. -3. Select **Reset demo DB** to start over. +Create a synthetic account and make a simulated deposit. Sign out, create another account on the same device and deposit again: there is no second welcome match. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, sign up from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The signup is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Welcome bonus](https://docs.shieldlabs.ai/tutorials/welcome-bonus) (the new guide stays local until its separate documentation release). diff --git a/bonus-abuse/package-lock.json b/bonus-abuse/package-lock.json index 660c464..2dfbd38 100644 --- a/bonus-abuse/package-lock.json +++ b/bonus-abuse/package-lock.json @@ -11,40 +11,17 @@ "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -258,82 +235,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", "license": "MIT", - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -342,16 +287,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -402,51 +337,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -476,21 +391,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -500,91 +400,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -605,16 +420,6 @@ ], "license": "MIT" }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -685,26 +490,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -712,54 +497,55 @@ "license": "ISC" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "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==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-3.0.0.tgz", @@ -786,61 +572,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -865,21 +596,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -908,9 +624,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", "dev": true, "license": "BSD-3-Clause" }, @@ -927,9 +643,9 @@ } }, "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==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, @@ -942,26 +658,6 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -978,72 +674,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1053,27 +683,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1083,14 +692,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-json-stringify": { @@ -1200,16 +802,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1246,37 +838,19 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1320,34 +894,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1368,23 +914,6 @@ ], "license": "BSD-3-Clause" }, - "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, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1397,16 +926,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", @@ -1416,60 +935,6 @@ "node": ">= 10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/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, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1545,12 +1010,18 @@ ], "license": "MIT" }, - "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==", + "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, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } }, "node_modules/lru-cache": { "version": "11.5.3", @@ -1631,12 +1102,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "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==", + "node_modules/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", @@ -1644,16 +1118,6 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1684,72 +1148,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1766,20 +1164,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, "node_modules/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1860,53 +1244,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1918,44 +1255,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -2002,16 +1338,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2021,16 +1347,6 @@ "node": ">=0.10.0" } }, - "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, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2192,47 +1508,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2242,17 +1517,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2271,18 +1535,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2293,31 +1545,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2357,26 +1614,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2413,13 +1650,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2433,20 +1663,12 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, "license": "MIT" }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2454,30 +1676,48 @@ "license": "MIT" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", "dev": true, "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2517,43 +1757,31 @@ } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { diff --git a/bonus-abuse/package.json b/bonus-abuse/package.json index 174fe12..c5d93e9 100644 --- a/bonus-abuse/package.json +++ b/bonus-abuse/package.json @@ -7,21 +7,25 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/bonus-abuse/public/index.html b/bonus-abuse/public/index.html index d256895..8302318 100644 --- a/bonus-abuse/public/index.html +++ b/bonus-abuse/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Acme Wallet

@@ -23,8 +24,7 @@

Create your account

type="email" autocomplete="username" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" value="tutorial@example.test" />
+ diff --git a/bonus-abuse/public/index.js b/bonus-abuse/public/index.js index 9fd19de..5d90544 100644 --- a/bonus-abuse/public/index.js +++ b/bonus-abuse/public/index.js @@ -10,7 +10,7 @@ const result = document.getElementById('result'); // Identify the signup and the deposit when the user starts filling in each form. const signupIdentification = identifyOnFirstFocus(signupForm); -const depositIdentification = identifyOnFirstFocus(depositForm); +const depositIdentification = identifyOnFirstFocus(depositForm, { signedIn: true }); // Show the view that matches this browser's session. await refreshAccount(); diff --git a/bonus-abuse/public/shieldlabs.js b/bonus-abuse/public/shieldlabs.js index 9aefc15..6be835e 100644 --- a/bonus-abuse/public/shieldlabs.js +++ b/bonus-abuse/public/shieldlabs.js @@ -1,62 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; } -// On signed-in pages: ties the visit to the account with its hashed id, never a raw -// email. Account-level risk and the High-Risk Events ShieldLabs detects on users, -// such as Account takeover, build on it. -export async function checkSignedInUser(hashedUserId) { - const shieldlabs = await snippet; - shieldlabs?.checkAuthenticatedUser(hashedUserId); +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/bonus-abuse/server/db.js b/bonus-abuse/server/db.js index bcf2c82..059bc64 100644 --- a/bonus-abuse/server/db.js +++ b/bonus-abuse/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/bonus-abuse/server/server.js b/bonus-abuse/server/server.js index e55e212..7321e1e 100644 --- a/bonus-abuse/server/server.js +++ b/bonus-abuse/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyCookie from '@fastify/cookie'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; @@ -8,21 +12,25 @@ import { deposit, getAccount, signOut, signUp } from './wallet.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyCookie); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Create an account and sign the session in immediately. app.post('/api/signup', async (request, reply) => { const { email, password, requestId } = request.body ?? {}; const { token, ...result } = await signUp({ email, password, requestId }); - if (token) reply.setCookie('session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); + if (token) reply.setCookie('shieldlabs_demo_bonus_abuse_session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); return reply.send(result); }); @@ -39,28 +47,33 @@ app.post('/api/deposit', async (request, reply) => { app.post('/api/logout', async (request, reply) => { signOut(sessionToken(request)); - reply.clearCookie('session', { path: '/' }); + reply.clearCookie('shieldlabs_demo_bonus_abuse_session', { path: '/' }); return reply.send({ success: true, message: 'Signed out.' }); }); // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} // Reads the session cookie without a cookie-parsing dependency. function sessionToken(request) { - const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)session=([a-f0-9]+)/); + const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)shieldlabs_demo_bonus_abuse_session=([a-f0-9]+)/); return match ? match[1] : null; } diff --git a/bonus-abuse/server/shieldlabs.js b/bonus-abuse/server/shieldlabs.js index 791eb66..e577332 100644 --- a/bonus-abuse/server/shieldlabs.js +++ b/bonus-abuse/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/bonus-abuse/server/wallet.js b/bonus-abuse/server/wallet.js index ddd03e5..d9bf9b9 100644 --- a/bonus-abuse/server/wallet.js +++ b/bonus-abuse/server/wallet.js @@ -45,7 +45,7 @@ export async function deposit(token, { amountCents, requestId }) { // Read the identification behind this deposit. Unverified, automated and // Dangerous deposits are refused. - const check = await verifyIdentification(requestId); + const check = await verifyIdentification(requestId, { expectedUserHid: hashUserId(account.email) }); if (!check.ok) { return { success: false, message: `Deposit refused: ${check.message}` }; } diff --git a/bonus-abuse/test/app.test.js b/bonus-abuse/test/app.test.js new file mode 100644 index 0000000..7de7d8f --- /dev/null +++ b/bonus-abuse/test/app.test.js @@ -0,0 +1,146 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/signup",{"email":"one@example.test","password":"synthetic-password"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('bonus-abuse: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('a welcome match is limited by device and bound to the signed-in account', async () => { + const signup = await act(); assert.equal(signup.body.success, true); + const me = (await app.inject({ url: '/api/me', headers: { cookie: signup.cookie } })).json(); + const wrong = await actAt('/api/deposit', { amountCents: 1000 }, { userHid: 'wrong-account' }, signup.cookie); + assert.equal(wrong.body.success, false); + const paid = await actAt('/api/deposit', { amountCents: 1000 }, { userHid: me.userHid }, signup.cookie); + assert.equal(paid.body.success, true); + const next = await act({ email: 'two@example.test' }); + const nextMe = (await app.inject({ url: '/api/me', headers: { cookie: next.cookie } })).json(); + const again = await actAt('/api/deposit', { amountCents: 1000 }, { userHid: nextMe.userHid }, next.cookie); + assert.equal(again.body.success, true); assert.match(again.body.message, /already claimed/); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/bonus-abuse/test/browser.test.js b/bonus-abuse/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/bonus-abuse/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/card-testing/.env.example b/card-testing/.env.example index f16d12c..b5a6d76 100644 --- a/card-testing/.env.example +++ b/card-testing/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/card-testing/LICENSE b/card-testing/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/card-testing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/card-testing/README.md b/card-testing/README.md index 95b8f67..45e3e04 100644 --- a/card-testing/README.md +++ b/card-testing/README.md @@ -1,66 +1,44 @@ -# ShieldLabs Card Testing Tutorial +# ShieldLabs Card testing tutorial -This tutorial shows how to stop card testing with ShieldLabs: declined card attempts are capped per device, and automated or Dangerous checkouts are refused before they reach the payment processor. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Declined attempts are counted per device**. -See the full guide at [Card Testing](https://docs.shieldlabs.ai/use-case/card-testing). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- card-testing`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -Payments are simulated: `4242 4242 4242 4242` is approved and any other valid card number (for example `4111 1111 1111 1111`) is declined. Nothing is charged. - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- card-testing`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the checkout form. The request ID of that identification goes to the server with the payment. -- `server/shieldlabs.js` reads the identification from the History API. The checkout is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/orders.js` stores the Device ID with every payment attempt. After 3 declined cards from one device in 24 hours, further checkouts from that device are refused before they reach the payment processor, even after the cookies are cleared or the IP address changes. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Buy a gift card with `4111 1111 1111 1111`. The card is declined. -2. Try two more declined card numbers, such as `4000 0000 0000 0002` and `5555 5555 5555 4444`. -3. Try `4242 4242 4242 4242`. The checkout is refused: this device reached the limit of 3 declined cards in 24 hours, so a card tester cannot keep trying cards. +Use only the sample cards: 4242424242424242 is approved and 4000000000000002 is declined. After three synthetic declines, another attempt from the same device is refused. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, try a card from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The checkout is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Card testing](https://docs.shieldlabs.ai/tutorials/card-testing) (the new guide stays local until its separate documentation release). diff --git a/card-testing/package-lock.json b/card-testing/package-lock.json index a64e141..7ea428d 100644 --- a/card-testing/package-lock.json +++ b/card-testing/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/card-testing/package.json b/card-testing/package.json index c126cf3..0e2da47 100644 --- a/card-testing/package.json +++ b/card-testing/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/card-testing/public/index.html b/card-testing/public/index.html index 9413c6e..4ece973 100644 --- a/card-testing/public/index.html +++ b/card-testing/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Gift Cards

Send a digital gift card

@@ -39,7 +40,7 @@

Send a digital gift card

+ diff --git a/card-testing/public/shieldlabs.js b/card-testing/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/card-testing/public/shieldlabs.js +++ b/card-testing/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/card-testing/server/db.js b/card-testing/server/db.js index fa7fb29..6474c45 100644 --- a/card-testing/server/db.js +++ b/card-testing/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/card-testing/server/server.js b/card-testing/server/server.js index 3527089..af8be6c 100644 --- a/card-testing/server/server.js +++ b/card-testing/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { purchaseGiftCard, recentAttempts } from './orders.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Buy a gift card. @@ -29,16 +37,21 @@ app.get('/api/attempts', async (_request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/card-testing/server/shieldlabs.js b/card-testing/server/shieldlabs.js index 791eb66..e577332 100644 --- a/card-testing/server/shieldlabs.js +++ b/card-testing/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/card-testing/test/app.test.js b/card-testing/test/app.test.js new file mode 100644 index 0000000..68d46b9 --- /dev/null +++ b/card-testing/test/app.test.js @@ -0,0 +1,139 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/purchase",{"recipientEmail":"one@example.test","amount":25,"cardNumber":"4242424242424242","expiry":"12/30","cvc":"123"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('card-testing: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('three synthetic declines stop a fourth payment on the same device', async () => { + for (let n=0;n<3;n++) assert.match((await act({ cardNumber: '4000000000000002' })).body.message, /declined/); + assert.match((await act()).body.message, /too many declined/); + assert.equal((await act({}, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/card-testing/test/browser.test.js b/card-testing/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/card-testing/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/chargeback-dispute/.env.example b/chargeback-dispute/.env.example index f16d12c..b5a6d76 100644 --- a/chargeback-dispute/.env.example +++ b/chargeback-dispute/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/chargeback-dispute/LICENSE b/chargeback-dispute/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/chargeback-dispute/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/chargeback-dispute/README.md b/chargeback-dispute/README.md index d0125c5..c0c7cc1 100644 --- a/chargeback-dispute/README.md +++ b/chargeback-dispute/README.md @@ -1,68 +1,44 @@ -# ShieldLabs Chargeback Dispute Tutorial +# ShieldLabs Chargeback evidence tutorial -This tutorial shows how to gather evidence against friendly-fraud chargebacks with ShieldLabs: every order stores the device, IP address and country behind it, so a disputed order can be shown next to the earlier orders the same device placed and never disputed. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Store the verified device behind an order**. -See the full guide at [Chargeback Fraud](https://docs.shieldlabs.ai/use-case/chargeback-fraud). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- chargeback-dispute`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below): the shop at `/` and the admin page at `/admin.html`. - -Payments are simulated: any card number is approved and nothing is charged. - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: - -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -## How it works +## Compare starter and final -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the buyer starts filling in the checkout form. The request ID of that identification goes to the server with the order. -- `server/shieldlabs.js` reads the identification from the History API. The order is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/orders.js` stores the Device ID, public IP address, country, Risk Score and request ID with every order. For a disputed order, the evidence lists the earlier orders from the same device that were never disputed, and the device's history read from the History API by Device ID. +Stop the server before switching versions. From the repository root run `git diff starter final -- chargeback-dispute`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -The three orders the demo starts with are seed data with made-up identification details: Riley placed both of Riley's orders from one device. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Buy tickets with the email `jordan@example.com`. -2. Clear the site's cookies, or open an incognito window, and buy tickets again with a different email, `jordan.work@example.com`. -3. On the admin page, simulate a chargeback on the second order and open its dispute evidence. It shows the first order, placed from the same device and never disputed, and the device's identifications in ShieldLabs History: evidence you can send with your dispute response. +Create two simulated orders, report a demo chargeback on the later order, and inspect the earlier undisputed same-device order and the bounded History summary. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, buy tickets from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The order is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Chargeback evidence](https://docs.shieldlabs.ai/tutorials/chargeback-evidence) (the new guide stays local until its separate documentation release). diff --git a/chargeback-dispute/package-lock.json b/chargeback-dispute/package-lock.json index ec04f39..716d725 100644 --- a/chargeback-dispute/package-lock.json +++ b/chargeback-dispute/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/chargeback-dispute/package.json b/chargeback-dispute/package.json index 93edc86..2a2380d 100644 --- a/chargeback-dispute/package.json +++ b/chargeback-dispute/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/chargeback-dispute/public/admin.html b/chargeback-dispute/public/admin.html index b55a823..bb5868f 100644 --- a/chargeback-dispute/public/admin.html +++ b/chargeback-dispute/public/admin.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Tickets admin

Back to the shop diff --git a/chargeback-dispute/public/index.html b/chargeback-dispute/public/index.html index f10e541..3b3ad7b 100644 --- a/chargeback-dispute/public/index.html +++ b/chargeback-dispute/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Tickets

Admin: orders and chargebacks @@ -32,7 +33,7 @@

Get tickets

+ diff --git a/chargeback-dispute/public/shieldlabs.js b/chargeback-dispute/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/chargeback-dispute/public/shieldlabs.js +++ b/chargeback-dispute/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/chargeback-dispute/server/db.js b/chargeback-dispute/server/db.js index 08256c9..c688703 100644 --- a/chargeback-dispute/server/db.js +++ b/chargeback-dispute/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/chargeback-dispute/server/orders.js b/chargeback-dispute/server/orders.js index a8d0126..737dd77 100644 --- a/chargeback-dispute/server/orders.js +++ b/chargeback-dispute/server/orders.js @@ -54,6 +54,7 @@ export async function placeOrder({ eventId, quantity, email, cardNumber, request const tickets = quantity === 1 ? '1 ticket' : `${quantity} tickets`; return { success: true, + orderId: Number(lastInsertRowid), message: `Order #${lastInsertRowid} confirmed: ${tickets} for ${event.name}, sent to ${email}.`, }; } @@ -92,10 +93,10 @@ export async function getEvidence(orderId) { // sign that the cardholder placed the disputed order too. const sameDevice = db .prepare( - `SELECT * FROM orders WHERE device_id = ? AND id != ? AND created_at < ? AND chargeback_at IS NULL - ORDER BY created_at DESC`, + `SELECT * FROM orders WHERE device_id = ? AND id < ? AND chargeback_at IS NULL + ORDER BY created_at DESC, id DESC`, ) - .all(order.device_id, order.id, order.created_at); + .all(order.device_id, order.id); return { success: true, @@ -114,13 +115,13 @@ function findOrder(id) { async function summarizeDevice(deviceId) { try { const rows = await readHistory('device_id', deviceId, 100); - const times = rows.map((row) => row.created_at).sort(); + const times = rows.map((row) => row.observed_at).sort(); return { identifications: rows.length, firstSeen: times[0] ?? null, lastSeen: times.at(-1) ?? null, - countries: [...new Set(rows.map((row) => row.country).filter(Boolean))], - publicIps: [...new Set(rows.map((row) => row.ip).filter(Boolean))], + countries: [...new Set(rows.map((row) => row.public_ip?.country).filter(Boolean))], + publicIps: [...new Set(rows.map((row) => row.public_ip?.ip).filter(Boolean))], }; } catch (error) { console.error(`[shieldlabs] device history: ${error.message}`); diff --git a/chargeback-dispute/server/server.js b/chargeback-dispute/server/server.js index 809789d..33ad24d 100644 --- a/chargeback-dispute/server/server.js +++ b/chargeback-dispute/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -8,13 +12,17 @@ import { fileChargeback, getEvidence, listOrders, placeOrder } from './orders.js initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // The shop. @@ -42,16 +50,21 @@ app.get('/api/orders/:id/evidence', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/chargeback-dispute/server/shieldlabs.js b/chargeback-dispute/server/shieldlabs.js index 791eb66..e577332 100644 --- a/chargeback-dispute/server/shieldlabs.js +++ b/chargeback-dispute/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/chargeback-dispute/test/app.test.js b/chargeback-dispute/test/app.test.js new file mode 100644 index 0000000..668fc36 --- /dev/null +++ b/chargeback-dispute/test/app.test.js @@ -0,0 +1,142 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/orders",{"eventId":1,"quantity":1,"email":"one@example.test","cardNumber":"4242424242424242"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('chargeback-dispute: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('dispute evidence uses normalized SDK History and earlier orders', async () => { + const first = await act(); const second = await act(); + assert.equal(first.body.success, true); assert.equal(second.body.success, true); + await app.inject({ method: 'POST', url: '/api/orders/' + second.body.orderId + '/chargeback' }); + const evidence = (await app.inject({ url: '/api/orders/' + second.body.orderId + '/evidence' })).json(); + assert.equal(evidence.success, true); assert.equal(evidence.sameDevice.length, 1); + assert.ok(evidence.deviceHistory.firstSeen); assert.deepEqual(evidence.deviceHistory.countries, ['US']); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/chargeback-dispute/test/browser.test.js b/chargeback-dispute/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/chargeback-dispute/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/checkout-risk/.env.example b/checkout-risk/.env.example new file mode 100644 index 0000000..85f47ef --- /dev/null +++ b/checkout-risk/.env.example @@ -0,0 +1,5 @@ +PORT=3000 +DEMO_ALLOW_RESET=1 +SHIELDLABS_PUBLIC_KEY=your-public-key +SHIELDLABS_API_KEY=sec_your_private_api_key +# SHIELDLABS_API_URL=https://account.shieldlabs.ai diff --git a/checkout-risk/.gitignore b/checkout-risk/.gitignore new file mode 100644 index 0000000..a98faa8 --- /dev/null +++ b/checkout-risk/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +db.sqlite* diff --git a/checkout-risk/LICENSE b/checkout-risk/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/checkout-risk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/checkout-risk/README.md b/checkout-risk/README.md new file mode 100644 index 0000000..0f069f3 --- /dev/null +++ b/checkout-risk/README.md @@ -0,0 +1,44 @@ +# ShieldLabs Checkout review tutorial + +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Dangerous orders go to review**. + +## Run this application + +Use Node.js 22 or later. From this folder: + +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` + +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. + +## Compare starter and final + +Stop the server before switching versions. From the repository root run `git diff starter final -- checkout-risk`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. + +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. + +## Try it + +Place an ordinary synthetic order, then inspect the automated high-risk test. No payment is made. + +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. + +## Verify + +```sh +npm run check +npm test +``` + +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. + +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. + +## Reset and production boundary + +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Checkout review](https://docs.shieldlabs.ai/tutorials/review-checkout) (the new guide stays local until its separate documentation release). diff --git a/checkout-risk/package-lock.json b/checkout-risk/package-lock.json new file mode 100644 index 0000000..0fb28ab --- /dev/null +++ b/checkout-risk/package-lock.json @@ -0,0 +1,1755 @@ +{ + "name": "shieldlabs-checkout-risk-tutorial", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shieldlabs-checkout-risk-tutorial", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", + "better-sqlite3": "^12.11.1", + "dotenv": "^17.4.2", + "fastify": "^5.12.5" + }, + "devDependencies": { + "puppeteer": "25.12.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.1.0.tgz", + "integrity": "sha512-F3EVbzWt+xcnVaOHmWyIlpuFtbxOln7HDZQsh09MtMmMm/CipMayNt8hnIL8VQi54u2ZociDbf+iluGYkf7B1A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz", + "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz", + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.1.tgz", + "integrity": "sha512-zv07Y9GEuDsJPegZoDFd4SDWaZOW8N2pa0GSrYmKpId/tjt1Hgo3BjZBVjdVpfVrHaA+Qv5jawtS2O50J5xM9g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.1.tgz", + "integrity": "sha512-BYo+EiaKwlxH+WetGk6hAs1d39iP0y1gqB8lGF/qwkJ9ZZ/cBY1vx5NvExb9Sc3yRMFjD5X4Eyh4e4+TzRkzdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.5.tgz", + "integrity": "sha512-3r0ZFXo82nKlPzySOUYhwWVfPZStfiwjmyevkX7cJWL2yXmEzl0Rpxv+L6L1R4OxqvlDGqf3g8OdBzILKgqe6w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/error": "^4.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^3.0.0", + "fastify-plugin": "^6.0.0", + "fastq": "^1.17.1", + "glob": "^13.0.0" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@puppeteer/browsers": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/chromium-bidi": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/content-disposition": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-3.0.0.tgz", + "integrity": "sha512-ZH/0Xs9rMIFWCOmGdmS9eHBTF62qqQYNz4nVjQhkdIO/a0fCP4UIM3mRz/wiqL0L14YgAz/1xio4OaSY4+ON/A==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.1.tgz", + "integrity": "sha512-TmHQgewjHtMq1E5QKA0tOE0yeYGQs25KZC/ziJpubRtWI15W92e6vPFydWOeZBVROSHBYw/QhD9d5OefdD6LDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.12.5", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.5.tgz", + "integrity": "sha512-OB2k1dlxs5/NAABqeKV2FUHkSD2BbENsCak8yULVcymn3fHIPDVa9TI3SDnJSWYSllZmSYuZXy2gTnsT+Sut1A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.1.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.9.0.tgz", + "integrity": "sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.97.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.97.0.tgz", + "integrity": "sha512-6Pu8F0eXGZlyz0EepPJrWgWDcBI8rKt2Wj8exOJU1tjVbu/VyZy5ZaLYS5N+Jp/Y3IP3sIRKcIOElTu7uC6KKA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pino": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.4.0.tgz", + "integrity": "sha512-bk1ZMTwG/Vymx+zPoa28MhNzucFvXJWp3csrSuxuHPSMeUettaCoQ5Jvg07mtC4JGNpcfTwz41oVCnNgBK9+vA==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^1.0.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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, + "license": "MIT", + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/checkout-risk/package.json b/checkout-risk/package.json new file mode 100644 index 0000000..5d3f2bd --- /dev/null +++ b/checkout-risk/package.json @@ -0,0 +1,30 @@ +{ + "name": "shieldlabs-checkout-risk-tutorial", + "version": "1.0.0", + "description": "Simulated checkout review with ShieldLabs", + "private": true, + "license": "MIT", + "type": "module", + "main": "server/server.js", + "engines": { + "node": ">=22" + }, + "scripts": { + "dev": "node --watch server/server.js", + "start": "node server/server.js", + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" + }, + "dependencies": { + "@fastify/static": "^10.1.5", + "better-sqlite3": "^12.11.1", + "dotenv": "^17.4.2", + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" + }, + "devDependencies": { + "puppeteer": "25.12.0" + } +} diff --git a/checkout-risk/public/index.html b/checkout-risk/public/index.html new file mode 100644 index 0000000..434aa2d --- /dev/null +++ b/checkout-risk/public/index.html @@ -0,0 +1,4 @@ + +ShieldLabs checkout review tutorial +

ShieldLabs tutorial · final

Review a risky checkout

Simulated orders only. No card details, payments or real purchases.

+ diff --git a/checkout-risk/public/index.js b/checkout-risk/public/index.js new file mode 100644 index 0000000..12d575d --- /dev/null +++ b/checkout-risk/public/index.js @@ -0,0 +1,13 @@ +import { identifyOnFirstFocus } from './shieldlabs.js'; +const form = document.querySelector('#checkoutForm'); +const output = document.querySelector('#result'); +const identification = identifyOnFirstFocus(form); +form.addEventListener('submit', async event => { + event.preventDefault(); const button=form.querySelector('button'); button.disabled=true; + try { + const requestId = await identification.take(); + const response = await fetch('/api/checkout',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({email:form.elements.email.value,requestId})}); + const data=await response.json(); output.textContent=data.message; + } catch { output.textContent='The demo could not complete this action.'; } finally { button.disabled=false; } +}); +document.querySelector('#resetDb').addEventListener('click', async () => {const response=await fetch('/api/reset-db',{method:'POST'});output.textContent=(await response.json()).message;}); diff --git a/checkout-risk/public/shieldlabs.js b/checkout-risk/public/shieldlabs.js new file mode 100644 index 0000000..6be835e --- /dev/null +++ b/checkout-risk/public/shieldlabs.js @@ -0,0 +1,52 @@ +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; + +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} + +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; +} + +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; + }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; +} diff --git a/checkout-risk/server/db.js b/checkout-risk/server/db.js new file mode 100644 index 0000000..c3bcb6b --- /dev/null +++ b/checkout-risk/server/db.js @@ -0,0 +1,8 @@ +import Database from 'better-sqlite3'; +import { fileURLToPath } from 'node:url'; +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export function initDb() { + db.exec('CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, email TEXT NOT NULL, outcome TEXT NOT NULL)'); + db.exec('CREATE TABLE IF NOT EXISTS used_request_ids (request_id TEXT PRIMARY KEY, used_at INTEGER NOT NULL)'); +} +export function resetDb() { db.exec('DELETE FROM orders'); db.exec('DELETE FROM used_request_ids'); } diff --git a/checkout-risk/server/server.js b/checkout-risk/server/server.js new file mode 100644 index 0000000..91f725c --- /dev/null +++ b/checkout-risk/server/server.js @@ -0,0 +1,36 @@ +import 'dotenv/config'; +import Fastify from 'fastify'; +import fastifyStatic from '@fastify/static'; +import { fileURLToPath } from 'node:url'; +import { db, initDb, resetDb } from './db.js'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { verifyIdentification, band, resetVerificationState } from './shieldlabs.js'; +initDb(); +export const app = Fastify({ bodyLimit: 16_384 }); +app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); +app.get('/config.js', async (_request, reply) => reply.header('cache-control','no-store').type('application/javascript').send('window.SHIELDLABS_PUBLIC_KEY = '+JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')+'; window.SHIELDLABS_CONFIGURED = '+Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')+';')); +app.post('/api/checkout', async (request, reply) => { + const { email, requestId } = request.body ?? {}; + if (typeof email !== 'string' || email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return { success: false, message: 'Enter a synthetic email.' }; + let outcome = 'accepted'; + const check = await verifyIdentification(requestId, { blockBands: [] }); + if (!check.ok) return { success: false, message: check.message }; + if (band(check.identification.risk_score) === 'Dangerous') outcome = 'review'; + const { lastInsertRowid } = db.prepare('INSERT INTO orders (email,outcome) VALUES (?,?)').run(email,outcome); + return { success: true, outcome, orderId: Number(lastInsertRowid), message: outcome==='review'?'Synthetic order held for review; no payment taken.':'Synthetic order recorded; no payment taken.' }; +}); +app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset disabled.' }); + resetVerificationState(); resetDb(); return { success: true, message: 'Demo reset.' }; +}); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log('Checkout tutorial: http://127.0.0.1:'+port); + for (const signal of ['SIGINT','SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode=1; })); +} diff --git a/checkout-risk/server/shieldlabs.js b/checkout-risk/server/shieldlabs.js new file mode 100644 index 0000000..e577332 --- /dev/null +++ b/checkout-risk/server/shieldlabs.js @@ -0,0 +1,84 @@ +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; +import { db } from './db.js'; + +export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} + +export function band(score) { + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); +} + +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; + } +} + +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; + const identification = await getIdentification(requestId); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); + return { ok: true, identification }; +} + +export async function readHistory(searchType, value, limit = 20) { + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; +} + +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', + }; + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; +} diff --git a/checkout-risk/test-bot.js b/checkout-risk/test-bot.js new file mode 100644 index 0000000..ab7f7a9 --- /dev/null +++ b/checkout-risk/test-bot.js @@ -0,0 +1,10 @@ +import puppeteer from 'puppeteer'; +// Optional, controlled demo test only. Never enter a real email or payment data. +const browser = await puppeteer.launch(); +try { + const page = await browser.newPage(); + await page.goto(process.env.BASE_URL || 'http://127.0.0.1:3000'); + await page.click('#checkoutForm button'); + await page.waitForFunction(() => document.querySelector('#result').textContent !== '', { timeout: 40_000 }); + console.log('Demo response:', await page.$eval('#result', element => element.textContent)); +} finally { await browser.close(); } diff --git a/checkout-risk/test/app.test.js b/checkout-risk/test/app.test.js new file mode 100644 index 0000000..fce0e78 --- /dev/null +++ b/checkout-risk/test/app.test.js @@ -0,0 +1,134 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/checkout",{"email":"tutorial@example.test"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('checkout-risk: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, name === 'Dangerous'); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); +test('checkout outcome stays server-owned and never returns a risk score', async () => {const result=await act({}, {score:80});assert.equal(result.body.outcome,'review');assert.ok(!Object.hasOwn(result.body,'risk_score'));}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, true); + assert.equal(result.body.outcome, 'review'); + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/checkout-risk/test/browser.test.js b/checkout-risk/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/checkout-risk/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/coupon-abuse/.env.example b/coupon-abuse/.env.example index f16d12c..b5a6d76 100644 --- a/coupon-abuse/.env.example +++ b/coupon-abuse/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/coupon-abuse/LICENSE b/coupon-abuse/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/coupon-abuse/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/coupon-abuse/README.md b/coupon-abuse/README.md index 2509a8e..417bc66 100644 --- a/coupon-abuse/README.md +++ b/coupon-abuse/README.md @@ -1,64 +1,44 @@ -# ShieldLabs Coupon Abuse Tutorial +# ShieldLabs Coupon abuse tutorial -This tutorial shows how to stop coupon abuse with ShieldLabs: each code can be redeemed once per device, a second code on the same device has to wait, and automated or Dangerous redemptions are refused. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **One use of a code per device, with a code cooldown**. -See the full guide at [Coupon Abuse](https://docs.shieldlabs.ai/use-case/coupon-abuse). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- coupon-abuse`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- coupon-abuse`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the shopper starts typing a code. The request ID of that identification goes to the server with the code. -- `server/shieldlabs.js` reads the identification from the History API. The redemption is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). This check runs before the code is looked up, so scripts guessing codes are refused too. -- `server/coupons.js` stores the Device ID with every redemption. A code already redeemed on the device is refused, and a second, different code on the same device has to wait an hour. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Apply `WELCOME20`. You get 20% off. -2. Clear the site's cookies, or open an incognito window, and apply `WELCOME20` again. It is refused: the code was already used on this device. -3. Apply `SPRING10`. It is refused for now: this device redeemed another code less than an hour ago. +Use WELCOME20, then repeat it from the same device. The second redemption is refused; another code has a one-hour demo cooldown. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, redeem `WELCOME20` from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The redemption is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Coupon abuse](https://docs.shieldlabs.ai/tutorials/coupon-limit) (the new guide stays local until its separate documentation release). diff --git a/coupon-abuse/package-lock.json b/coupon-abuse/package-lock.json index 2f9c594..53846e8 100644 --- a/coupon-abuse/package-lock.json +++ b/coupon-abuse/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/coupon-abuse/package.json b/coupon-abuse/package.json index 09d7289..dadcea9 100644 --- a/coupon-abuse/package.json +++ b/coupon-abuse/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/coupon-abuse/public/index.html b/coupon-abuse/public/index.html index 6c05761..ac4f4dd 100644 --- a/coupon-abuse/public/index.html +++ b/coupon-abuse/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Outdoor Shop

Your cart

@@ -21,8 +22,7 @@

Your cart

placeholder="Coupon code" autocomplete="off" required - class="flex-1 rounded-md border border-slate-300 px-3 py-2 uppercase" - /> + class="flex-1 rounded-md border border-slate-300 px-3 py-2 uppercase" value="WELCOME20" />
+ diff --git a/coupon-abuse/public/shieldlabs.js b/coupon-abuse/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/coupon-abuse/public/shieldlabs.js +++ b/coupon-abuse/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/coupon-abuse/server/db.js b/coupon-abuse/server/db.js index 201ad8b..f7b35ca 100644 --- a/coupon-abuse/server/db.js +++ b/coupon-abuse/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/coupon-abuse/server/server.js b/coupon-abuse/server/server.js index e94d2be..abbd8aa 100644 --- a/coupon-abuse/server/server.js +++ b/coupon-abuse/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { applyCoupon, getCart } from './coupons.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); app.get('/api/cart', async (_request, reply) => { @@ -28,16 +36,21 @@ app.post('/api/coupons/apply', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/coupon-abuse/server/shieldlabs.js b/coupon-abuse/server/shieldlabs.js index 791eb66..e577332 100644 --- a/coupon-abuse/server/shieldlabs.js +++ b/coupon-abuse/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/coupon-abuse/test/app.test.js b/coupon-abuse/test/app.test.js new file mode 100644 index 0000000..b541d68 --- /dev/null +++ b/coupon-abuse/test/app.test.js @@ -0,0 +1,138 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/coupons/apply",{"code":"WELCOME20"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('coupon-abuse: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('a coupon remains used after a changed browser session', async () => { + assert.equal((await act()).body.success, true); assert.equal((await act()).body.success, false); + assert.equal((await act({}, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/coupon-abuse/test/browser.test.js b/coupon-abuse/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/coupon-abuse/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/credential-stuffing/.env.example b/credential-stuffing/.env.example index f16d12c..b5a6d76 100644 --- a/credential-stuffing/.env.example +++ b/credential-stuffing/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/credential-stuffing/LICENSE b/credential-stuffing/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/credential-stuffing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/credential-stuffing/README.md b/credential-stuffing/README.md index 3a1ec04..a0fd493 100644 --- a/credential-stuffing/README.md +++ b/credential-stuffing/README.md @@ -1,64 +1,44 @@ -# ShieldLabs Credential Stuffing Tutorial +# ShieldLabs Credential stuffing tutorial -This tutorial shows how to stop credential stuffing with ShieldLabs: failed sign-ins are counted per device, automated sign-ins are refused, and a correct password from a device the account has never used gets a verification challenge. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Failed passwords are counted per device**. -See the full guide at [Credential Stuffing](https://docs.shieldlabs.ai/use-case/credential-stuffing). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- credential-stuffing`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- credential-stuffing`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the sign-in form. The request ID of that identification goes to the server with the credentials. -- `server/shieldlabs.js` reads the identification from the History API. The attempt is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/accounts.js` counts failed sign-ins per Device ID: after 5 failures in 24 hours, the device is refused, even when the script rotates IP addresses or clears cookies. A correct password from a device the account has never used gets a one-time code by email first (simulated: the code is shown on the page). In the demo, the first device an account ever signs in from becomes its first known device. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Sign in with `demo@example.com` / `demo-password`. This browser becomes the account's known device. -2. Sign in from a different browser with the same password. You are asked for a one-time code, as a script with a leaked password would be. Enter the code shown on the page to finish. -3. In a browser, enter 5 wrong passwords. The next attempt is refused, even with the right password: this device reached the limit of failed sign-ins. +Make five synthetic failed sign-ins, then try a valid password on that device. The sixth attempt is refused. A correct password from an unfamiliar device can require the displayed demo code. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, try a leaked password from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The attempt is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Credential stuffing](https://docs.shieldlabs.ai/tutorials/credential-stuffing) (the new guide stays local until its separate documentation release). diff --git a/credential-stuffing/package-lock.json b/credential-stuffing/package-lock.json index 8b776d0..80d7b33 100644 --- a/credential-stuffing/package-lock.json +++ b/credential-stuffing/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/credential-stuffing/package.json b/credential-stuffing/package.json index 6f428a8..b6db1a2 100644 --- a/credential-stuffing/package.json +++ b/credential-stuffing/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/credential-stuffing/public/index.html b/credential-stuffing/public/index.html index 69df3df..cb16b00 100644 --- a/credential-stuffing/public/index.html +++ b/credential-stuffing/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Account

Sign in

@@ -20,8 +21,7 @@

Sign in

type="email" autocomplete="username" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" value="demo@example.com" />
+ diff --git a/credential-stuffing/public/index.js b/credential-stuffing/public/index.js index ab35a31..fe77b7d 100644 --- a/credential-stuffing/public/index.js +++ b/credential-stuffing/public/index.js @@ -52,6 +52,8 @@ function showCodeForm(challenge) { challengeId = challenge.challengeId; document.getElementById('demoCode').textContent = challenge.demoCode; codeForm.reset(); + // Demo-only: the simulator returns this code openly; no real email is sent. + codeForm.elements.code.value = challenge.demoCode; } codeForm.classList.toggle('hidden', !challenge); } diff --git a/credential-stuffing/public/shieldlabs.js b/credential-stuffing/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/credential-stuffing/public/shieldlabs.js +++ b/credential-stuffing/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/credential-stuffing/server/db.js b/credential-stuffing/server/db.js index 8a66889..e7b3b98 100644 --- a/credential-stuffing/server/db.js +++ b/credential-stuffing/server/db.js @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'; import { hashPassword } from './passwords.js'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/credential-stuffing/server/server.js b/credential-stuffing/server/server.js index 65c5477..6198aa8 100644 --- a/credential-stuffing/server/server.js +++ b/credential-stuffing/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { signIn, verifyCode } from './accounts.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Sign in. @@ -30,16 +38,21 @@ app.post('/api/verify-code', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/credential-stuffing/server/shieldlabs.js b/credential-stuffing/server/shieldlabs.js index 791eb66..e577332 100644 --- a/credential-stuffing/server/shieldlabs.js +++ b/credential-stuffing/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/credential-stuffing/test/app.test.js b/credential-stuffing/test/app.test.js new file mode 100644 index 0000000..52695ff --- /dev/null +++ b/credential-stuffing/test/app.test.js @@ -0,0 +1,139 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/login",{"email":"demo@example.com","password":"demo-password"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('credential-stuffing: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('five failed passwords stop the next attempt from the same device', async () => { + for(let n=0;n<5;n++) assert.equal((await act({ password: 'synthetic-wrong' })).body.success, false); + assert.match((await act()).body.message, /too many failed/); + assert.equal((await act({}, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/credential-stuffing/test/browser.test.js b/credential-stuffing/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/credential-stuffing/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/loan-risk/.env.example b/loan-risk/.env.example index f16d12c..b5a6d76 100644 --- a/loan-risk/.env.example +++ b/loan-risk/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/loan-risk/LICENSE b/loan-risk/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/loan-risk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/loan-risk/README.md b/loan-risk/README.md index 39513b5..069c996 100644 --- a/loan-risk/README.md +++ b/loan-risk/README.md @@ -1,61 +1,44 @@ -# ShieldLabs Loan Application Fraud Tutorial +# ShieldLabs Loan application review tutorial -This tutorial shows how to catch loan application fraud with ShieldLabs: when the same device resubmits an application with a different name or income, the application is flagged for review instead of getting an instant offer. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Conflicting same-device applications need review**. -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- loan-risk`. +## Run this application -## Setup +Use Node.js 22 or later. From this folder: -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- loan-risk`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the applicant starts filling in the form. The request ID of that identification goes to the server with the application. -- `server/shieldlabs.js` reads the identification from the History API. The application is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/loans.js` stores the Device ID with every application and compares each new application with the ones from the same device in the last 24 hours. A different name or income is flagged for manual review and no offer is calculated, even when the applicant clears cookies or switches to an incognito window. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Apply as Alex Morgan with a monthly income of 2,000 for a 20,000 loan over 12 months. The application is declined: the payment is too high for that income. -2. Apply again with the same details and a monthly income of 9,000. The application is flagged for manual review, with no offer: this device applied a moment ago with a different income. +Submit made-up applicant details, then change the name or income on the same device. The changed application is flagged for review. No real lending decision is made. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, apply from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The application is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Loan application review](https://docs.shieldlabs.ai/tutorials/loan-application-review) (the new guide stays local until its separate documentation release). diff --git a/loan-risk/package-lock.json b/loan-risk/package-lock.json index dc4a4e4..9bc7791 100644 --- a/loan-risk/package-lock.json +++ b/loan-risk/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/loan-risk/package.json b/loan-risk/package.json index ff7d08c..9820a93 100644 --- a/loan-risk/package.json +++ b/loan-risk/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/loan-risk/public/index.html b/loan-risk/public/index.html index e044007..9417eab 100644 --- a/loan-risk/public/index.html +++ b/loan-risk/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Lending

Check your personal loan offer

An instant decision, based on what you tell us.

@@ -16,21 +17,21 @@

Check your personal loan offer

+ diff --git a/loan-risk/public/shieldlabs.js b/loan-risk/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/loan-risk/public/shieldlabs.js +++ b/loan-risk/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/loan-risk/server/db.js b/loan-risk/server/db.js index a29b052..117a37c 100644 --- a/loan-risk/server/db.js +++ b/loan-risk/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/loan-risk/server/server.js b/loan-risk/server/server.js index 90e6620..3dd5e0b 100644 --- a/loan-risk/server/server.js +++ b/loan-risk/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { applyForLoan } from './loans.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Apply for a loan. @@ -24,16 +32,21 @@ app.post('/api/applications', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/loan-risk/server/shieldlabs.js b/loan-risk/server/shieldlabs.js index 791eb66..e577332 100644 --- a/loan-risk/server/shieldlabs.js +++ b/loan-risk/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/loan-risk/test/app.test.js b/loan-risk/test/app.test.js new file mode 100644 index 0000000..752814d --- /dev/null +++ b/loan-risk/test/app.test.js @@ -0,0 +1,139 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/applications",{"firstName":"Demo","lastName":"Applicant","monthlyIncome":5000,"amount":1000,"termMonths":12}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('loan-risk: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('conflicting synthetic applications from one device require review', async () => { + assert.equal((await act()).body.success, true); + assert.match((await act({ firstName: 'Other' })).body.message, /manual review/); + assert.equal((await act({ firstName: 'Other' }, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/loan-risk/test/browser.test.js b/loan-risk/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/loan-risk/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/new-account-fraud/.env.example b/new-account-fraud/.env.example index f16d12c..b5a6d76 100644 --- a/new-account-fraud/.env.example +++ b/new-account-fraud/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/new-account-fraud/LICENSE b/new-account-fraud/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/new-account-fraud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/new-account-fraud/README.md b/new-account-fraud/README.md index e1e3978..eae37de 100644 --- a/new-account-fraud/README.md +++ b/new-account-fraud/README.md @@ -1,64 +1,44 @@ -# ShieldLabs New Account Fraud Tutorial +# ShieldLabs New account fraud tutorial -This tutorial shows how to stop new account fraud, such as free trial abuse and multi-accounting, with ShieldLabs: one free trial per device, and no trial for automated or Dangerous signups. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **One trial per Device ID**. -See the full guide at [New Account Fraud](https://docs.shieldlabs.ai/use-case/new-account-fraud). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- new-account-fraud`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- new-account-fraud`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the signup form. The request ID of that identification goes to the server with the signup. -- `server/shieldlabs.js` reads the identification from the History API. The signup is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/accounts.js` stores the Device ID with each trial account and refuses a second trial from the same device. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Start a trial with any username and password. -2. Clear the site's cookies, or open an incognito window, and start another trial with a different username. It is refused: the Device ID is the same. -3. Select **Reset demo DB** to start over. +Create a trial, then try another username on the same device. The second trial is refused. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, sign up from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The signup is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [New account fraud](https://docs.shieldlabs.ai/tutorials/one-trial-per-device) (the new guide stays local until its separate documentation release). diff --git a/new-account-fraud/package-lock.json b/new-account-fraud/package-lock.json index e508404..8ef387f 100644 --- a/new-account-fraud/package-lock.json +++ b/new-account-fraud/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/new-account-fraud/package.json b/new-account-fraud/package.json index 8e7f2b8..7ee46fa 100644 --- a/new-account-fraud/package.json +++ b/new-account-fraud/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/new-account-fraud/public/index.html b/new-account-fraud/public/index.html index 460e66e..20d0365 100644 --- a/new-account-fraud/public/index.html +++ b/new-account-fraud/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Notes

Start your 14-day free trial

No credit card needed. One free trial per person.

@@ -20,8 +21,7 @@

Start your 14-day free trial

name="username" autocomplete="username" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" value="tutorial_one" />
+ diff --git a/new-account-fraud/public/shieldlabs.js b/new-account-fraud/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/new-account-fraud/public/shieldlabs.js +++ b/new-account-fraud/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/new-account-fraud/server/db.js b/new-account-fraud/server/db.js index 93c2e5f..5460040 100644 --- a/new-account-fraud/server/db.js +++ b/new-account-fraud/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/new-account-fraud/server/server.js b/new-account-fraud/server/server.js index 6803cfd..fa85225 100644 --- a/new-account-fraud/server/server.js +++ b/new-account-fraud/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { signUp } from './accounts.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Start a free trial. @@ -24,16 +32,21 @@ app.post('/api/signup', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/new-account-fraud/server/shieldlabs.js b/new-account-fraud/server/shieldlabs.js index 791eb66..e577332 100644 --- a/new-account-fraud/server/shieldlabs.js +++ b/new-account-fraud/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/new-account-fraud/test/app.test.js b/new-account-fraud/test/app.test.js new file mode 100644 index 0000000..3d68cd5 --- /dev/null +++ b/new-account-fraud/test/app.test.js @@ -0,0 +1,139 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/signup",{"username":"tutorial_one","password":"synthetic-password"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('new-account-fraud: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('another username does not earn another trial on the same device', async () => { + assert.equal((await act()).body.success, true); + assert.equal((await act({ username: 'tutorial_two' })).body.success, false); + assert.equal((await act({ username: 'tutorial_two' }, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/new-account-fraud/test/browser.test.js b/new-account-fraud/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/new-account-fraud/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/paywall/.env.example b/paywall/.env.example index f16d12c..b5a6d76 100644 --- a/paywall/.env.example +++ b/paywall/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/paywall/LICENSE b/paywall/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/paywall/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/paywall/README.md b/paywall/README.md index d8b5753..9020b3a 100644 --- a/paywall/README.md +++ b/paywall/README.md @@ -1,64 +1,44 @@ -# ShieldLabs Paywall Tutorial +# ShieldLabs Paywall enforcement tutorial -This tutorial shows how to enforce a metered paywall with ShieldLabs: free articles are counted per device, so opening an incognito window or clearing cookies does not reset the count. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Two distinct free articles per device per day**. -See the full guide at [Paywall](https://docs.shieldlabs.ai/use-case/paywall). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- paywall`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- paywall`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet, and the article page runs `forceCheckAnonymous` on every view. The request ID of that identification goes to the server with the request for the article. -- `server/shieldlabs.js` reads the identification from the History API. The article stays locked when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/articles.js` meters free articles by Device ID instead of a cookie: 2 distinct articles every 24 hours. Opening an article already read today does not count again. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Read two articles. The third one is locked. -2. Open the site in an incognito window, or clear the site's cookies, and open the third article again. It stays locked: the Device ID is the same, so the meter is too. -3. Open one of the two articles you already read. It still opens. +Read two different articles, then a third. A new browser session does not reset the device meter. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, open an article from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The article stays locked: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the article list, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Paywall enforcement](https://docs.shieldlabs.ai/tutorials/one-free-article) (the new guide stays local until its separate documentation release). diff --git a/paywall/package-lock.json b/paywall/package-lock.json index 92efc87..1684241 100644 --- a/paywall/package-lock.json +++ b/paywall/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/paywall/package.json b/paywall/package.json index 1ab47bd..539fe59 100644 --- a/paywall/package.json +++ b/paywall/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/paywall/public/article.html b/paywall/public/article.html index 2ce7257..4cf6aab 100644 --- a/paywall/public/article.html +++ b/paywall/public/article.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

The Millbrook Ledger

All articles @@ -24,6 +25,7 @@

+ diff --git a/paywall/public/index.html b/paywall/public/index.html index 472f4c9..3098ba7 100644 --- a/paywall/public/index.html +++ b/paywall/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

The Millbrook Ledger

Local news

Read 2 articles free every day. Subscribe for unlimited reading.

diff --git a/paywall/public/shieldlabs.js b/paywall/public/shieldlabs.js index 12eef55..6be835e 100644 --- a/paywall/public/shieldlabs.js +++ b/paywall/public/shieldlabs.js @@ -1,37 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; +} + +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; + }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/paywall/server/db.js b/paywall/server/db.js index 659d3c7..4ea8752 100644 --- a/paywall/server/db.js +++ b/paywall/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/paywall/server/server.js b/paywall/server/server.js index 7a9ddcd..eacbdec 100644 --- a/paywall/server/server.js +++ b/paywall/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { listArticles, readArticle } from './articles.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); app.get('/api/articles', async (_request, reply) => { @@ -27,16 +35,21 @@ app.post('/api/articles/:id/read', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/paywall/server/shieldlabs.js b/paywall/server/shieldlabs.js index 791eb66..e577332 100644 --- a/paywall/server/shieldlabs.js +++ b/paywall/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/paywall/test/app.test.js b/paywall/test/app.test.js new file mode 100644 index 0000000..2ea987b --- /dev/null +++ b/paywall/test/app.test.js @@ -0,0 +1,142 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/articles/1/read",{}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('paywall: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + const article = await app.inject({ url: '/article.html?id=1' }); + assert.equal(article.statusCode, 200); + assert.match(article.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('two different articles exhaust the device meter, not another device', async () => { + assert.equal((await act()).body.success, true); + assert.equal((await actAt('/api/articles/2/read', {})).body.success, true); + assert.equal((await actAt('/api/articles/3/read', {})).body.success, false); + assert.equal((await actAt('/api/articles/3/read', {}, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/paywall/test/browser.test.js b/paywall/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/paywall/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/personalization/.env.example b/personalization/.env.example index f16d12c..b5a6d76 100644 --- a/personalization/.env.example +++ b/personalization/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/personalization/LICENSE b/personalization/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/personalization/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/personalization/README.md b/personalization/README.md index a5b6881..e0d4215 100644 --- a/personalization/README.md +++ b/personalization/README.md @@ -1,64 +1,44 @@ -# ShieldLabs Returning Visitor Tutorial +# ShieldLabs Returning visitor personalization tutorial -This tutorial shows how to recognize a returning visitor with ShieldLabs: search history and saved items are keyed to the device, so they are still there in an incognito window or after the visitor clears cookies. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Preferences follow a verified device rather than only its session cookie**. -See the full guide at [Returning Visitor](https://docs.shieldlabs.ai/use-case/returning-visitor). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without ShieldLabs. See what the integration adds with `git diff starter final -- personalization`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- personalization`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- When the browser has no session yet (a first visit, an incognito window, cleared cookies), the page runs `forceCheckAnonymous` once through `public/shieldlabs.js` and sends the request ID to the server. -- `server/shieldlabs.js` reads the identification from the History API. Personalization stays off when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/sessions.js` maps a session cookie to the Device ID, and `server/store.js` keys the recent searches and saved items by that Device ID. A new cookie on the same device finds the same history. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Search for `lamp` and `mug`, and save a couple of products. -2. Open the store in an incognito window, or clear the site's cookies and reload. Your recent searches and saved items are still there: the Device ID is the same. -3. Open the store in a different browser. It starts empty: that is another device. +Search for a sample product, then start another session on the same observed device. The prior search is remembered. Do not put sensitive preferences in this example. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, search and save a product from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -Personalization stays off for the bot: headless Chrome raises the Browser Automation signal, so nothing is saved for it. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Returning visitor personalization](https://docs.shieldlabs.ai/tutorials/returning-device) (the new guide stays local until its separate documentation release). diff --git a/personalization/package-lock.json b/personalization/package-lock.json index 876bb11..f925cfb 100644 --- a/personalization/package-lock.json +++ b/personalization/package-lock.json @@ -11,38 +11,17 @@ "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -291,76 +270,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -369,15 +322,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -428,47 +372,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -498,20 +426,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -521,86 +435,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -620,15 +454,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -695,124 +520,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -837,20 +597,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -877,10 +623,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -894,10 +641,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -907,29 +655,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -939,67 +670,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1008,26 +678,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1040,12 +690,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1153,15 +797,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1191,39 +826,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1265,32 +885,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1310,22 +904,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1336,64 +914,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1469,19 +989,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1543,33 +1061,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1598,68 +1112,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1685,18 +1137,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1776,40 +1216,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1820,42 +1226,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1900,15 +1307,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1918,15 +1316,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2083,44 +1472,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2130,16 +1481,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2157,17 +1498,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2177,29 +1507,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2236,24 +1573,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2289,12 +1608,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2307,17 +1620,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2325,28 +1632,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2357,6 +1684,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2378,45 +1706,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2424,6 +1744,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/personalization/package.json b/personalization/package.json index f84e27a..df4ffb1 100644 --- a/personalization/package.json +++ b/personalization/package.json @@ -7,21 +7,25 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/personalization/public/index.html b/personalization/public/index.html index 164e84c..eb02eda 100644 --- a/personalization/public/index.html +++ b/personalization/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Home Goods

Find something for your home

@@ -50,6 +51,7 @@

Saved items

+ diff --git a/personalization/public/shieldlabs.js b/personalization/public/shieldlabs.js index 12eef55..6be835e 100644 --- a/personalization/public/shieldlabs.js +++ b/personalization/public/shieldlabs.js @@ -1,37 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; +} + +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; + }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/personalization/server/db.js b/personalization/server/db.js index 37fbabb..7a701ac 100644 --- a/personalization/server/db.js +++ b/personalization/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/personalization/server/server.js b/personalization/server/server.js index b229077..20ca2f9 100644 --- a/personalization/server/server.js +++ b/personalization/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyCookie from '@fastify/cookie'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; @@ -9,14 +13,18 @@ import { getProfile, search, toggleSaved } from './store.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyCookie); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Identify a browser that has no session yet. Its Device ID is the shopper. @@ -24,7 +32,7 @@ app.post('/api/session', async (request, reply) => { const { token, deviceId, ...result } = await startDeviceSession(request.body?.requestId); if (!token) return reply.send(result); - reply.setCookie('device_session', token, { httpOnly: true, sameSite: 'lax', path: '/' }); + reply.setCookie('shieldlabs_demo_personalization_device', token, { httpOnly: true, sameSite: 'lax', path: '/' }); const profile = getProfile(deviceId); const returning = profile.recentSearches.length > 0 || profile.saved.length > 0; return reply.send({ ...result, returning, profile: { identified: true, ...profile } }); @@ -50,22 +58,27 @@ app.post('/api/saved', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} // The shopper behind a request: the Device ID of the browser's session, or null. function shopperFor(request) { - const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)device_session=([a-f0-9]+)/); + const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)shieldlabs_demo_personalization_device=([a-f0-9]+)/); return deviceForSession(match ? match[1] : null); } diff --git a/personalization/server/shieldlabs.js b/personalization/server/shieldlabs.js index 791eb66..e577332 100644 --- a/personalization/server/shieldlabs.js +++ b/personalization/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/personalization/test/app.test.js b/personalization/test/app.test.js new file mode 100644 index 0000000..6fcb2ba --- /dev/null +++ b/personalization/test/app.test.js @@ -0,0 +1,141 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/session",{}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('personalization: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('a new session on the same device retrieves saved synthetic preferences', async () => { + const first = await act(); assert.equal(first.body.success, true); + await app.inject({ url: '/api/search?q=lamp', headers: { cookie: first.cookie } }); + const again = await act(); assert.equal(again.body.returning, true); + assert.deepEqual(again.body.profile.recentSearches, ['lamp']); + assert.equal((await act({}, { device: DEVICE_B })).body.returning, false); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/personalization/test/browser.test.js b/personalization/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/personalization/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/promo-abuse/.env.example b/promo-abuse/.env.example index f16d12c..b5a6d76 100644 --- a/promo-abuse/.env.example +++ b/promo-abuse/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/promo-abuse/LICENSE b/promo-abuse/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/promo-abuse/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/promo-abuse/README.md b/promo-abuse/README.md index 76cae25..5a4a553 100644 --- a/promo-abuse/README.md +++ b/promo-abuse/README.md @@ -1,63 +1,44 @@ -# ShieldLabs Promo Abuse Tutorial +# ShieldLabs First-order promotion tutorial -This tutorial shows how to stop promo abuse with ShieldLabs: new customers get 20% off their first order automatically, based on whether their device has ordered before, so a new email address or cleared cookies cannot claim the discount again. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **One first-order discount per device**. -See the full guide at [Promo Abuse](https://docs.shieldlabs.ai/use-case/promo-abuse). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- promo-abuse`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- promo-abuse`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the shopper starts filling in the checkout form. The request ID of that identification goes to the server with the order. -- `server/shieldlabs.js` reads the identification from the History API. The order is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/orders.js` grants the 20% first-order discount once per Device ID, not once per email: the Device ID stays the same when cookies are cleared, in an incognito window and on a new IP address. The "20% off" banner on the cart page is a preview only, driven by a plain cookie that is not a security control; the checkout call above is what actually decides the discount. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Check out with the email `jordan@example.com`. You get 20% off. -2. Clear the site's cookies, or open an incognito window, and check out again with a different email, `jordan2@example.com`. You are charged full price: the message says you already used your first-order discount. +Place a simulated first order for one email, then use another email on the same device. The second order receives no first-order discount. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, check out from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The order is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [First-order promotion](https://docs.shieldlabs.ai/tutorials/first-order-promo) (the new guide stays local until its separate documentation release). diff --git a/promo-abuse/package-lock.json b/promo-abuse/package-lock.json index 04213bf..9bce9d1 100644 --- a/promo-abuse/package-lock.json +++ b/promo-abuse/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -224,76 +203,50 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -301,15 +254,6 @@ "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -357,47 +301,31 @@ ] }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -425,20 +353,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -447,86 +361,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -546,15 +380,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -620,74 +445,61 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "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==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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/content-disposition": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-3.0.0.tgz", @@ -712,58 +524,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", - "dev": true, - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -786,20 +546,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -825,10 +571,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -842,10 +589,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -855,29 +603,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -887,67 +618,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -956,26 +626,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -986,12 +636,6 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1093,15 +737,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1130,39 +765,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1203,32 +823,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1248,22 +842,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1274,15 +852,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", @@ -1291,55 +860,6 @@ "node": ">= 10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1398,11 +918,18 @@ } ] }, - "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/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, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } }, "node_modules/lru-cache": { "version": "11.5.3", @@ -1468,33 +995,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1522,68 +1045,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1599,18 +1060,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1686,49 +1135,6 @@ } ] }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1739,42 +1145,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1817,15 +1224,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1834,15 +1232,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -1992,44 +1381,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2038,16 +1389,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2064,17 +1405,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2084,29 +1414,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2143,24 +1480,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2193,12 +1512,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2211,17 +1524,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2229,28 +1536,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2261,6 +1588,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2282,45 +1610,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2328,6 +1648,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/promo-abuse/package.json b/promo-abuse/package.json index 0366810..ea79877 100644 --- a/promo-abuse/package.json +++ b/promo-abuse/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/promo-abuse/public/index.html b/promo-abuse/public/index.html index 5f8b2a9..f198071 100644 --- a/promo-abuse/public/index.html +++ b/promo-abuse/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Acme Supply Co

Your cart

@@ -30,8 +31,7 @@

Your cart

type="email" autocomplete="email" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" value="tutorial@example.test" />
+ diff --git a/promo-abuse/public/shieldlabs.js b/promo-abuse/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/promo-abuse/public/shieldlabs.js +++ b/promo-abuse/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/promo-abuse/server/db.js b/promo-abuse/server/db.js index da0f31c..b708975 100644 --- a/promo-abuse/server/db.js +++ b/promo-abuse/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/promo-abuse/server/orders.js b/promo-abuse/server/orders.js index 5fceb46..9fa1472 100644 --- a/promo-abuse/server/orders.js +++ b/promo-abuse/server/orders.js @@ -20,7 +20,7 @@ const SUBTOTAL = CART.reduce((sum, item) => sum + item.price, 0); // that is fine, because it is not what decides the discount. The checkout // call below re-checks eligibility against the ShieldLabs Device ID, which // holds through cleared cookies, incognito windows and new IP addresses. -const HINT_COOKIE = 'acme_ordered_before'; +const HINT_COOKIE = 'shieldlabs_demo_promo_ordered_before'; export function getCart(request) { return { items: CART, subtotal: SUBTOTAL, firstOrderEligible: !hasOrderedBeforeHint(request) }; diff --git a/promo-abuse/server/server.js b/promo-abuse/server/server.js index 719be6b..1287f26 100644 --- a/promo-abuse/server/server.js +++ b/promo-abuse/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { checkout, clearOrderedBeforeHint, getCart } from './orders.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); app.get('/api/cart', async (request, reply) => { @@ -28,6 +36,8 @@ app.post('/api/checkout', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); clearOrderedBeforeHint(reply); return reply.send({ success: true, message: 'Demo database reset.' }); @@ -35,10 +45,13 @@ app.post('/api/reset-db', async (_request, reply) => { // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/promo-abuse/server/shieldlabs.js b/promo-abuse/server/shieldlabs.js index 791eb66..e577332 100644 --- a/promo-abuse/server/shieldlabs.js +++ b/promo-abuse/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/promo-abuse/test/app.test.js b/promo-abuse/test/app.test.js new file mode 100644 index 0000000..14518d1 --- /dev/null +++ b/promo-abuse/test/app.test.js @@ -0,0 +1,139 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/checkout",{"email":"one@example.test"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('promo-abuse: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('a changed email does not reset the first-order discount', async () => { + assert.ok((await act()).body.discount > 0); + assert.equal((await act({ email: 'two@example.test' })).body.discount, 0); + assert.ok((await act({ email: 'three@example.test' }, { device: DEVICE_B })).body.discount > 0); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/promo-abuse/test/browser.test.js b/promo-abuse/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/promo-abuse/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/referral-fraud/.env.example b/referral-fraud/.env.example index f16d12c..b5a6d76 100644 --- a/referral-fraud/.env.example +++ b/referral-fraud/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/referral-fraud/LICENSE b/referral-fraud/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/referral-fraud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/referral-fraud/README.md b/referral-fraud/README.md index 3bbc499..1ab4537 100644 --- a/referral-fraud/README.md +++ b/referral-fraud/README.md @@ -1,64 +1,44 @@ -# ShieldLabs Referral Fraud Tutorial +# ShieldLabs Referral fraud tutorial -This tutorial shows how to stop referral fraud with ShieldLabs: a referral reward is paid only when the new signup and the referrer are on different devices, so one person cannot farm the reward by signing up "friends" from their own browser. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Same-device referrals earn no reward**. -See the full guide at [Referral Fraud](https://docs.shieldlabs.ai/use-case/referral-fraud). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- referral-fraud`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- referral-fraud`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the signup form. The request ID of that identification goes to the server with the signup. -- `server/shieldlabs.js` reads the identification from the History API. The signup is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). The account is still created either way; only the identification is checked here, not the referral itself. -- `server/accounts.js` stores the Device ID behind every signup, including the referrer's own signup. When a new signup enters a referral code, the reward is paid only if its Device ID differs from the Device ID stored on the referrer's account. If the two match, the account is created normally but neither side gets the $10, and the response says so. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Sign up with `riley@example.com`. Note the referral code shown, for example `RILEY482`. -2. In the same browser, sign up a second account, `riley.alt@example.com`, using that referral code. The account is created, but the reward is refused: you referred yourself. The Device ID is the same as riley's. -3. Open the app in a different browser, or on your phone, and sign up `casey@example.com` with the same code. This time both accounts earn $10: the Device ID is different. An incognito window on the same browser keeps the same Device ID, so it is refused like step 2. +Create a synthetic referrer and use their code on a new account from the same device: no credit is earned. A separately observed device can earn the simulated reward. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, sign up from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The signup is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Referral fraud](https://docs.shieldlabs.ai/tutorials/referral-pair) (the new guide stays local until its separate documentation release). diff --git a/referral-fraud/package-lock.json b/referral-fraud/package-lock.json index db076df..96d80aa 100644 --- a/referral-fraud/package-lock.json +++ b/referral-fraud/package-lock.json @@ -11,40 +11,17 @@ "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -258,82 +235,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", "license": "MIT", - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -342,16 +287,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -402,51 +337,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -476,21 +391,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -500,91 +400,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -605,16 +420,6 @@ ], "license": "MIT" }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -685,26 +490,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -712,54 +497,55 @@ "license": "ISC" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "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==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-3.0.0.tgz", @@ -786,61 +572,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -865,21 +596,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -908,9 +624,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", "dev": true, "license": "BSD-3-Clause" }, @@ -927,9 +643,9 @@ } }, "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==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, @@ -942,26 +658,6 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -978,72 +674,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1053,27 +683,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1083,14 +692,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-json-stringify": { @@ -1200,16 +802,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1246,37 +838,19 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1320,34 +894,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1368,23 +914,6 @@ ], "license": "BSD-3-Clause" }, - "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, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1397,16 +926,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", @@ -1416,60 +935,6 @@ "node": ">= 10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/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, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1545,12 +1010,18 @@ ], "license": "MIT" }, - "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==", + "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, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } }, "node_modules/lru-cache": { "version": "11.5.3", @@ -1631,12 +1102,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "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==", + "node_modules/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", @@ -1644,16 +1118,6 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1684,72 +1148,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1766,20 +1164,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, "node_modules/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1860,53 +1244,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1918,44 +1255,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -2002,16 +1338,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2021,16 +1347,6 @@ "node": ">=0.10.0" } }, - "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, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2192,47 +1508,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2242,17 +1517,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2271,18 +1535,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2293,31 +1545,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2357,26 +1614,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2413,13 +1650,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2433,20 +1663,12 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, "license": "MIT" }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2454,30 +1676,48 @@ "license": "MIT" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", "dev": true, "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2517,43 +1757,31 @@ } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { diff --git a/referral-fraud/package.json b/referral-fraud/package.json index 9ecfcc5..494519b 100644 --- a/referral-fraud/package.json +++ b/referral-fraud/package.json @@ -7,21 +7,25 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/cookie": "^11.1.2", "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/referral-fraud/public/index.html b/referral-fraud/public/index.html index afec75f..466283a 100644 --- a/referral-fraud/public/index.html +++ b/referral-fraud/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Acme Fit

@@ -23,8 +24,7 @@

Create your account

type="email" autocomplete="email" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" value="tutorial@example.test" />
+ diff --git a/referral-fraud/public/shieldlabs.js b/referral-fraud/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/referral-fraud/public/shieldlabs.js +++ b/referral-fraud/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/referral-fraud/server/db.js b/referral-fraud/server/db.js index 6f11ea4..7214668 100644 --- a/referral-fraud/server/db.js +++ b/referral-fraud/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/referral-fraud/server/server.js b/referral-fraud/server/server.js index 8fc8642..d2db8e4 100644 --- a/referral-fraud/server/server.js +++ b/referral-fraud/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyCookie from '@fastify/cookie'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; @@ -8,14 +12,18 @@ import { getSession, signUp, endSession } from './accounts.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyCookie); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Create an account and, if a referral code was entered, credit the reward. @@ -23,7 +31,7 @@ app.post('/api/signup', async (request, reply) => { const { email, referralCode, requestId } = request.body ?? {}; const result = await signUp({ email, referralCode, requestId }); if (result.success) { - reply.setCookie('session', result.token, { httpOnly: true, sameSite: 'lax', path: '/' }); + reply.setCookie('shieldlabs_demo_referral_fraud_session', result.token, { httpOnly: true, sameSite: 'lax', path: '/' }); } return reply.send({ success: result.success, @@ -40,28 +48,33 @@ app.get('/api/me', async (request, reply) => { app.post('/api/logout', async (request, reply) => { endSession(sessionToken(request)); - reply.clearCookie('session', { path: '/' }); + reply.clearCookie('shieldlabs_demo_referral_fraud_session', { path: '/' }); return reply.send({ success: true, message: 'Signed out.' }); }); // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} // Reads the session cookie without a cookie-parsing dependency. function sessionToken(request) { - const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)session=([a-f0-9]+)/); + const match = (request.headers.cookie ?? '').match(/(?:^|;\s*)shieldlabs_demo_referral_fraud_session=([a-f0-9]+)/); return match ? match[1] : null; } diff --git a/referral-fraud/server/shieldlabs.js b/referral-fraud/server/shieldlabs.js index 791eb66..e577332 100644 --- a/referral-fraud/server/shieldlabs.js +++ b/referral-fraud/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/referral-fraud/test/app.test.js b/referral-fraud/test/app.test.js new file mode 100644 index 0000000..1cbeecf --- /dev/null +++ b/referral-fraud/test/app.test.js @@ -0,0 +1,141 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/signup",{"email":"one@example.test"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('referral-fraud: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('same-device referral earns no reward, another device earns the demo credit', async () => { + const owner = await act(); assert.equal(owner.body.success, true); + const ownCode = (await app.inject({ url: '/api/me', headers: { cookie: owner.cookie } })).json().referralCode; + const self = await act({ email: 'two@example.test', referralCode: ownCode }); + assert.equal(self.body.creditCents, 0); + assert.equal((await act({ email: 'three@example.test', referralCode: ownCode }, { device: DEVICE_B })).body.creditCents, 1000); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/referral-fraud/test/browser.test.js b/referral-fraud/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/referral-fraud/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/regional-pricing/.env.example b/regional-pricing/.env.example index f16d12c..b5a6d76 100644 --- a/regional-pricing/.env.example +++ b/regional-pricing/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/regional-pricing/LICENSE b/regional-pricing/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/regional-pricing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/regional-pricing/README.md b/regional-pricing/README.md index e7ba642..5a01325 100644 --- a/regional-pricing/README.md +++ b/regional-pricing/README.md @@ -1,66 +1,44 @@ -# ShieldLabs Regional Pricing Tutorial +# ShieldLabs Regional pricing tutorial -This tutorial shows how to enforce regional pricing with ShieldLabs: a regional discount applies only when the country of the shopper's connection matches it and the connection is not masked by a VPN, a proxy, Tor or a privacy relay. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **A regional price needs a matching unmasked connection**. -See the full guide at [Regional Pricing](https://docs.shieldlabs.ai/use-case/regional-pricing). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- regional-pricing`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -The discount tiers in `server/tiers.js` are illustrative, made up for the demo. - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- regional-pricing`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the shopper starts picking a country. The request ID of that identification goes to the server with the selected country. -- `server/shieldlabs.js` reads the identification from the History API. The list price stays when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/pricing.js` applies the regional price only when the connection is not masked (VPN, proxy, Tor, privacy relay, datacenter IP address or a browser VPN or proxy extension), the time zone of the device matches its location, and the country of the public IP address is the country the shopper selected. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Select the country you are in and activate regional pricing. You get that country's price. -2. Select India (unless you are there) and activate regional pricing. It is refused: the connection is in another country. -3. Connect through a VPN with an exit in India and try again. It is still refused: the connection is masked. +Select a sample country and request its regional price. A VPN/proxy or a mismatch with the observed IP country refuses the regional discount. No purchase is made. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, claim the India price from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The request is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Regional pricing](https://docs.shieldlabs.ai/tutorials/regional-discount) (the new guide stays local until its separate documentation release). diff --git a/regional-pricing/package-lock.json b/regional-pricing/package-lock.json index 7126efd..15b303d 100644 --- a/regional-pricing/package-lock.json +++ b/regional-pricing/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/regional-pricing/package.json b/regional-pricing/package.json index 943c4fb..d651fa5 100644 --- a/regional-pricing/package.json +++ b/regional-pricing/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/regional-pricing/public/index.html b/regional-pricing/public/index.html index 164241b..01c9b4c 100644 --- a/regional-pricing/public/index.html +++ b/regional-pricing/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Design Studio

Checkout

@@ -52,6 +53,7 @@

Checkout

+ diff --git a/regional-pricing/public/shieldlabs.js b/regional-pricing/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/regional-pricing/public/shieldlabs.js +++ b/regional-pricing/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/regional-pricing/server/db.js b/regional-pricing/server/db.js index 4f5e1f9..706e9a5 100644 --- a/regional-pricing/server/db.js +++ b/regional-pricing/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/regional-pricing/server/pricing.js b/regional-pricing/server/pricing.js index d9183d1..d6e2449 100644 --- a/regional-pricing/server/pricing.js +++ b/regional-pricing/server/pricing.js @@ -33,6 +33,7 @@ export async function activateRegionalPrice({ country, requestId }) { flags.tor || flags.privacy_relay || flags.datacenter_ip || + flags.browser_vpn_proxy || connectionType === 'browser_vpn_proxy'; if (masked) { return { success: false, message: 'Regional pricing is not available over a VPN, a proxy or Tor. Turn it off and try again.' }; diff --git a/regional-pricing/server/server.js b/regional-pricing/server/server.js index d10ff8d..1890a1d 100644 --- a/regional-pricing/server/server.js +++ b/regional-pricing/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { activateRegionalPrice, getPricing } from './pricing.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // The list price and the countries with their discount. @@ -29,16 +37,21 @@ app.post('/api/regional-price', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/regional-pricing/server/shieldlabs.js b/regional-pricing/server/shieldlabs.js index 791eb66..e577332 100644 --- a/regional-pricing/server/shieldlabs.js +++ b/regional-pricing/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/regional-pricing/test/app.test.js b/regional-pricing/test/app.test.js new file mode 100644 index 0000000..88ac656 --- /dev/null +++ b/regional-pricing/test/app.test.js @@ -0,0 +1,140 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/regional-price",{"country":"US"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('regional-pricing: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('masked and mismatched countries do not receive a regional price', async () => { + assert.equal((await act()).body.success, true); + assert.equal((await act({}, { vpn: true })).body.success, false); + assert.equal((await act({ country: 'IN' })).body.success, false); + assert.equal((await act({ country: 'IN' }, { country: 'IN' })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/regional-pricing/test/browser.test.js b/regional-pricing/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/regional-pricing/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/scripts/check-tutorial.mjs b/scripts/check-tutorial.mjs new file mode 100644 index 0000000..beda9fd --- /dev/null +++ b/scripts/check-tutorial.mjs @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict'; +import { readdir, readFile, stat } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +export const applications = [ + "account-sharing", + "account-takeover", + "ban-evasion", + "bonus-abuse", + "card-testing", + "chargeback-dispute", + "checkout-risk", + "coupon-abuse", + "credential-stuffing", + "loan-risk", + "new-account-fraud", + "paywall", + "personalization", + "promo-abuse", + "referral-fraud", + "regional-pricing", + "sms-pumping", + "survey-fraud", + "sybil-attack", + "web-scraping" +]; + +async function exists(path) { + try { return await stat(path); } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } +} + +export async function checkTutorial(name, repository = root) { + assert.ok(applications.includes(name), 'Unknown tutorial folder'); + const folder = join(repository, name); + for (const file of ['README.md', 'LICENSE', '.env.example', 'package.json', 'package-lock.json', 'public/index.html', 'public/index.js', 'server/server.js', 'server/db.js', 'test/app.test.js']) { + assert.ok((await exists(join(folder, file)))?.isFile(), name + ': missing ' + file); + } + const manifest = JSON.parse(await readFile(join(folder, 'package.json'), 'utf8')); + const lock = JSON.parse(await readFile(join(folder, 'package-lock.json'), 'utf8')); + assert.equal(manifest.private, true, name + ': teaching app must not be an npm release'); + assert.equal(manifest.engines.node, '>=22'); + assert.deepEqual(lock.packages[''].dependencies, manifest.dependencies); + assert.deepEqual(lock.packages[''].devDependencies, manifest.devDependencies); + assert.ok(manifest.scripts.test.includes('node --test'), name + ': native tests missing'); + const final = Boolean(manifest.dependencies['@shieldlabs-ai/node']); + if (final) { + for (const sdk of ['@shieldlabs-ai/js', '@shieldlabs-ai/node']) { + assert.equal(manifest.dependencies[sdk], '1.0.1', name + ': SDK must be pinned'); + } + assert.ok((await exists(join(folder, 'public/shieldlabs.js')))?.isFile()); + assert.ok((await exists(join(folder, 'server/shieldlabs.js')))?.isFile()); + assert.ok((await exists(join(folder, 'test/browser.test.js')))?.isFile()); + const browser = await readFile(join(folder, 'public/shieldlabs.js'), 'utf8'); + assert.ok(!browser.includes('SHIELDLABS_API_KEY'), name + ': private API key must stay server-side'); + } else { + assert.ok(!manifest.dependencies['@shieldlabs-ai/js'], name + ': starter must work without an SDK'); + } + for (const dependency of Object.values(manifest.dependencies)) { + assert.ok(!/^(?:file:|link:|\.\.?\/|\/)/.test(dependency), name + ': dependency points outside this folder'); + } + const env = await readFile(join(folder, '.env.example'), 'utf8'); + const values = Object.fromEntries(env.split('\n').filter(line => /^[A-Z_]+=/.test(line)).map(line => { + const separator = line.indexOf('='); + return [line.slice(0, separator), line.slice(separator + 1)]; + })); + if (final) { + assert.equal(values.SHIELDLABS_PUBLIC_KEY, 'your-public-key'); + assert.equal(values.SHIELDLABS_API_KEY, 'sec_your_private_api_key'); + } + for (const sourceFolder of ['public', 'server', 'test']) { + for (const file of await readdir(join(folder, sourceFolder))) { + if (!/\.m?js$/.test(file)) continue; + const source = join(folder, sourceFolder, file); + const result = spawnSync(process.execPath, ['--check', source], { encoding: 'utf8', timeout: 10_000 }); + assert.equal(result.status, 0, name + ': syntax error in ' + file + '\n' + (result.stderr ?? '')); + } + } + return { application: name, flavor: final ? 'final' : 'starter' }; +} + +export async function checkInventory(repository = root) { + const folders = []; + for (const entry of await readdir(repository, { withFileTypes: true })) { + if (entry.isDirectory() && (await exists(join(repository, entry.name, 'package.json')))?.isFile()) folders.push(entry.name); + } + assert.deepEqual(folders.sort(), [...applications].sort(), 'The tutorial matrix must cover every app'); +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + await checkInventory(); + for (const name of process.argv[2] ? [process.argv[2]] : applications) { + console.log(JSON.stringify(await checkTutorial(name))); + } +} diff --git a/sms-pumping/.env.example b/sms-pumping/.env.example index f16d12c..b5a6d76 100644 --- a/sms-pumping/.env.example +++ b/sms-pumping/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/sms-pumping/LICENSE b/sms-pumping/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/sms-pumping/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sms-pumping/README.md b/sms-pumping/README.md index d54bdb7..e47003d 100644 --- a/sms-pumping/README.md +++ b/sms-pumping/README.md @@ -1,66 +1,44 @@ -# ShieldLabs SMS Pumping Tutorial +# ShieldLabs SMS pumping tutorial -This tutorial shows how to stop SMS pumping with ShieldLabs: verification codes are capped per device with a growing wait between them, and automated, Tor or Dangerous requests never reach your SMS provider. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Three synthetic sends per device per day, with growing waits**. -See the full guide at [SMS Pumping](https://docs.shieldlabs.ai/use-case/sms-pumping). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- sms-pumping`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -The demo never sends a real SMS: the code is shown on the page instead. - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- sms-pumping`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` for every code request, resends included. The request ID of that identification goes to the server with the phone number. -- `server/shieldlabs.js` reads the identification from the History API. The request is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). `server/sms.js` also refuses requests over Tor. -- `server/sms.js` caps codes per Device ID: 3 a day, with a wait of 30 seconds before the second code and 60 seconds before the third. Clearing cookies, opening an incognito window or entering another phone number does not reset it. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Enter a phone number with its country code and select **Send code**. The code appears in the demo phone box. -2. Select **Send code** again right away. It is refused: wait 30 seconds before the second code. -3. After 30 seconds, send the second code, and after 60 more seconds, the third. A fourth code the same day is refused, even with another phone number or from an incognito window. +Request a sample code. Changing the phone number does not reset the device limit. The waits are 30 and 60 seconds before later sends; Tor and automation are refused. No SMS is sent. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, request a code from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The request is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [SMS pumping](https://docs.shieldlabs.ai/tutorials/verification-sends) (the new guide stays local until its separate documentation release). diff --git a/sms-pumping/package-lock.json b/sms-pumping/package-lock.json index 0a2fd82..1848386 100644 --- a/sms-pumping/package-lock.json +++ b/sms-pumping/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/sms-pumping/package.json b/sms-pumping/package.json index 3e9b0e0..ae775a1 100644 --- a/sms-pumping/package.json +++ b/sms-pumping/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/sms-pumping/public/index.html b/sms-pumping/public/index.html index 3b7fe74..6acb264 100644 --- a/sms-pumping/public/index.html +++ b/sms-pumping/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Chat

Verify your phone number

We send a 6-digit code by SMS to finish creating your account.

@@ -22,8 +23,7 @@

Verify your phone number

autocomplete="tel" placeholder="+1 555 010 0123" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2" value="+15550100123" />
+ diff --git a/sms-pumping/public/index.js b/sms-pumping/public/index.js index 807a3e6..8353c39 100644 --- a/sms-pumping/public/index.js +++ b/sms-pumping/public/index.js @@ -21,6 +21,8 @@ phoneForm.addEventListener('submit', async (event) => { if (data.success) { // Stands in for the SMS arriving on the user's phone. document.getElementById('smsCode').textContent = data.demoCode; + // Demo-only code returned by our local simulator, not a real SMS secret. + codeForm.elements.code.value = data.demoCode; document.getElementById('smsBox').classList.remove('hidden'); codeForm.classList.remove('hidden'); } diff --git a/sms-pumping/public/shieldlabs.js b/sms-pumping/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/sms-pumping/public/shieldlabs.js +++ b/sms-pumping/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/sms-pumping/server/db.js b/sms-pumping/server/db.js index 96f9db1..28ebdec 100644 --- a/sms-pumping/server/db.js +++ b/sms-pumping/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/sms-pumping/server/server.js b/sms-pumping/server/server.js index a07d52f..c7069af 100644 --- a/sms-pumping/server/server.js +++ b/sms-pumping/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { sendCode, verifyCode } from './sms.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Send a verification code to a phone number. @@ -29,16 +37,21 @@ app.post('/api/verify-code', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/sms-pumping/server/shieldlabs.js b/sms-pumping/server/shieldlabs.js index 791eb66..e577332 100644 --- a/sms-pumping/server/shieldlabs.js +++ b/sms-pumping/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/sms-pumping/test/app.test.js b/sms-pumping/test/app.test.js new file mode 100644 index 0000000..3fe2b92 --- /dev/null +++ b/sms-pumping/test/app.test.js @@ -0,0 +1,142 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/send-code",{"phone":"+15550100123"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('sms-pumping: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('changing recipients does not reset the synthetic daily SMS limit', async () => { + for(let n=0;n<3;n++) { + db.prepare('UPDATE sms_codes SET created_at = ?').run(Date.now()-61_000); + assert.equal((await act({ phone: '+1555010012'+n })).body.success, true); + } + assert.match((await act({ phone: '+15550100999' })).body.message, /limit/); + assert.equal((await act({}, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/sms-pumping/test/browser.test.js b/sms-pumping/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/sms-pumping/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/survey-fraud/.env.example b/survey-fraud/.env.example index f16d12c..b5a6d76 100644 --- a/survey-fraud/.env.example +++ b/survey-fraud/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/survey-fraud/LICENSE b/survey-fraud/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/survey-fraud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/survey-fraud/README.md b/survey-fraud/README.md index 7d9afbc..5757574 100644 --- a/survey-fraud/README.md +++ b/survey-fraud/README.md @@ -1,61 +1,44 @@ -# ShieldLabs Survey Fraud Tutorial +# ShieldLabs Survey fraud tutorial -This tutorial shows how to stop survey fraud with ShieldLabs: a paid survey accepts one submission per device, and automated or Dangerous submissions are refused. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **One reward-bearing survey per device**. -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- survey-fraud`. +## Run this application -## Setup +Use Node.js 22 or later. From this folder: -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- survey-fraud`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the respondent starts answering. The request ID of that identification goes to the server with the answers. -- `server/shieldlabs.js` reads the identification from the History API. The submission is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/survey.js` stores the Device ID with every submission and pays one reward per device: a new email address on the same device is refused. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Answer the survey with `sam@example.com`. The $5 reward is paid. -2. Clear the site's cookies, or open an incognito window, and answer again with `sam+2@example.com`. It is refused: this device has already taken the survey. +Complete the sample survey, then change the email and repeat from the same device. The second reward-bearing submission is refused. No gift card is sent. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, submit the survey from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The submission is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Survey fraud](https://docs.shieldlabs.ai/tutorials/rewarded-survey) (the new guide stays local until its separate documentation release). diff --git a/survey-fraud/package-lock.json b/survey-fraud/package-lock.json index 3d1501f..11ada49 100644 --- a/survey-fraud/package-lock.json +++ b/survey-fraud/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/survey-fraud/package.json b/survey-fraud/package.json index e4c1742..805b3ad 100644 --- a/survey-fraud/package.json +++ b/survey-fraud/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/survey-fraud/public/index.html b/survey-fraud/public/index.html index 16aa4ab..c3cf549 100644 --- a/survey-fraud/public/index.html +++ b/survey-fraud/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Research

Kitchen habits survey

Three questions, about two minutes. Get a $5 gift card for your answers.

@@ -44,7 +45,7 @@

Kitchen habits survey

+ diff --git a/survey-fraud/public/shieldlabs.js b/survey-fraud/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/survey-fraud/public/shieldlabs.js +++ b/survey-fraud/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/survey-fraud/server/db.js b/survey-fraud/server/db.js index 46f5050..1b8734c 100644 --- a/survey-fraud/server/db.js +++ b/survey-fraud/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/survey-fraud/server/server.js b/survey-fraud/server/server.js index 6d5051b..618103f 100644 --- a/survey-fraud/server/server.js +++ b/survey-fraud/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { submitSurvey } from './survey.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Submit the survey. @@ -24,16 +32,21 @@ app.post('/api/survey', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/survey-fraud/server/shieldlabs.js b/survey-fraud/server/shieldlabs.js index 791eb66..e577332 100644 --- a/survey-fraud/server/shieldlabs.js +++ b/survey-fraud/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/survey-fraud/test/app.test.js b/survey-fraud/test/app.test.js new file mode 100644 index 0000000..25e038f --- /dev/null +++ b/survey-fraud/test/app.test.js @@ -0,0 +1,139 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/survey",{"email":"one@example.test","answers":{"cooking":"daily","nextAppliance":"oven","groceries":"weekly"}}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('survey-fraud: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('another email does not earn another survey reward on the same device', async () => { + assert.equal((await act()).body.success, true); + assert.equal((await act({ email: 'two@example.test' })).body.success, false); + assert.equal((await act({ email: 'two@example.test' }, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/survey-fraud/test/browser.test.js b/survey-fraud/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/survey-fraud/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/sybil-attack/.env.example b/sybil-attack/.env.example index f16d12c..b5a6d76 100644 --- a/sybil-attack/.env.example +++ b/sybil-attack/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/sybil-attack/LICENSE b/sybil-attack/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/sybil-attack/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sybil-attack/README.md b/sybil-attack/README.md index fce81e0..0eaf653 100644 --- a/sybil-attack/README.md +++ b/sybil-attack/README.md @@ -1,64 +1,44 @@ -# ShieldLabs Sybil Attack Tutorial +# ShieldLabs Sybil claims tutorial -This tutorial shows how to stop a sybil attack on a token airdrop with ShieldLabs: one claim per device, no matter how many wallet addresses the visitor tries. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **One simulated airdrop claim per device**. -See the full guide at [Sybil Attack](https://docs.shieldlabs.ai/use-case/sybil-attack). +## Run this application -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- sybil-attack`. +Use Node.js 22 or later. From this folder: -## Setup - -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- sybil-attack`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` when the user starts filling in the claim form. The request ID of that identification goes to the server with the claim. -- `server/shieldlabs.js` reads the identification from the History API. The claim is refused when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/claims.js` stores the Device ID with each claim and refuses a second claim from the same device, no matter which wallet address it uses. A sybil attacker who generates a new wallet for every claim still hits the same device gate. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Claim with wallet `0xAAA1112223334445556667778889990001112223`. 100 ACME tokens are claimed. -2. Clear the site's cookies, or open an incognito window, and claim with a different wallet, `0xBBB9998887776665554443332221110009998887`. It is refused: the Device ID is the same, so the airdrop already sees this device as claimed, with the earlier wallet. -3. Select **Reset demo DB** to start over. +Claim with a sample wallet alias, then change the alias on the same device. The second claim is refused. No tokens are transferred. -## Run the bot test +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -With the server and the tunnel running, claim from headless Chrome: +## Verify -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The claim is refused: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Sybil claims](https://docs.shieldlabs.ai/tutorials/sybil-claim) (the new guide stays local until its separate documentation release). diff --git a/sybil-attack/package-lock.json b/sybil-attack/package-lock.json index 887a46e..908b77d 100644 --- a/sybil-attack/package-lock.json +++ b/sybil-attack/package-lock.json @@ -10,40 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -237,82 +214,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", "license": "MIT", - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -321,16 +266,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -381,51 +316,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -455,21 +370,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -479,91 +379,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -584,16 +399,6 @@ ], "license": "MIT" }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -664,26 +469,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "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, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -691,54 +476,55 @@ "license": "ISC" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "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==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-3.0.0.tgz", @@ -765,61 +551,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -844,21 +575,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -887,9 +603,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", "dev": true, "license": "BSD-3-Clause" }, @@ -906,9 +622,9 @@ } }, "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==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, @@ -921,26 +637,6 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -957,72 +653,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1032,27 +662,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1062,14 +671,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true, + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, "node_modules/fast-json-stringify": { @@ -1179,16 +781,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1225,37 +817,19 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1299,34 +873,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1347,23 +893,6 @@ ], "license": "BSD-3-Clause" }, - "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, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1376,16 +905,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", @@ -1395,60 +914,6 @@ "node": ">= 10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/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, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1511,12 +976,18 @@ ], "license": "MIT" }, - "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==", + "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, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } }, "node_modules/lru-cache": { "version": "11.5.3", @@ -1597,12 +1068,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "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==", + "node_modules/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", @@ -1610,16 +1084,6 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1650,72 +1114,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1732,20 +1130,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, "node_modules/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1826,53 +1210,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1884,44 +1221,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1968,16 +1304,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1987,16 +1313,6 @@ "node": ">=0.10.0" } }, - "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, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2158,47 +1474,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2208,17 +1483,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2237,18 +1501,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2259,31 +1511,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2323,26 +1580,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2379,13 +1616,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2399,20 +1629,12 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, "license": "MIT" }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2420,30 +1642,48 @@ "license": "MIT" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", "dev": true, "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2483,43 +1723,31 @@ } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { diff --git a/sybil-attack/package.json b/sybil-attack/package.json index 0a39b26..034c4bc 100644 --- a/sybil-attack/package.json +++ b/sybil-attack/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/sybil-attack/public/index.html b/sybil-attack/public/index.html index dfa1033..79dc063 100644 --- a/sybil-attack/public/index.html +++ b/sybil-attack/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Acme Chain Airdrop

Claim your ACME tokens

@@ -23,8 +24,7 @@

Claim your ACME tokens

placeholder="0x..." autocomplete="off" required - class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 font-mono text-sm" - /> + class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 font-mono text-sm" value="0xabcdef12" />
+ diff --git a/sybil-attack/public/shieldlabs.js b/sybil-attack/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/sybil-attack/public/shieldlabs.js +++ b/sybil-attack/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/sybil-attack/server/db.js b/sybil-attack/server/db.js index b3e7002..25c4b35 100644 --- a/sybil-attack/server/db.js +++ b/sybil-attack/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/sybil-attack/server/server.js b/sybil-attack/server/server.js index aef5bf6..490fe67 100644 --- a/sybil-attack/server/server.js +++ b/sybil-attack/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { claim, claimCount } from './claims.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); // Claim 100 simulated ACME tokens. @@ -29,16 +37,21 @@ app.get('/api/claims', async (_request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/sybil-attack/server/shieldlabs.js b/sybil-attack/server/shieldlabs.js index 791eb66..e577332 100644 --- a/sybil-attack/server/shieldlabs.js +++ b/sybil-attack/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/sybil-attack/test/app.test.js b/sybil-attack/test/app.test.js new file mode 100644 index 0000000..76ad4a3 --- /dev/null +++ b/sybil-attack/test/app.test.js @@ -0,0 +1,139 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/claim",{"walletAddress":"0xabcdef12"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('sybil-attack: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('another wallet does not create another same-device airdrop claim', async () => { + assert.equal((await act()).body.success, true); + assert.equal((await act({ walletAddress: '0xfedcba98' })).body.success, false); + assert.equal((await act({ walletAddress: '0xfedcba98' }, { device: DEVICE_B })).body.success, true); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/sybil-attack/test/browser.test.js b/sybil-attack/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/sybil-attack/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +}); diff --git a/web-scraping/.env.example b/web-scraping/.env.example index f16d12c..b5a6d76 100644 --- a/web-scraping/.env.example +++ b/web-scraping/.env.example @@ -7,3 +7,6 @@ SHIELDLABS_API_KEY=sec_your_private_api_key # Optional: the History API host. # SHIELDLABS_API_URL=https://account.shieldlabs.ai + +# Disposable local demo only: permits clearing its SQLite state. +DEMO_ALLOW_RESET=1 diff --git a/web-scraping/LICENSE b/web-scraping/LICENSE new file mode 100644 index 0000000..062f9dd --- /dev/null +++ b/web-scraping/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web-scraping/README.md b/web-scraping/README.md index 3941c1b..1e13ca5 100644 --- a/web-scraping/README.md +++ b/web-scraping/README.md @@ -1,71 +1,44 @@ -# ShieldLabs Web Scraping Tutorial +# ShieldLabs Web scraping tutorial -This tutorial shows how to protect proprietary data from scrapers with ShieldLabs: the flight search API serves prices to real browsers and refuses automated requests and Dangerous traffic. +This folder is a standalone application, on the **final** branch. Its `starter` version runs without ShieldLabs; its `final` version adds the pinned JS and Node SDKs and the following teaching rule: **Unverified or automated searches receive no flight prices**. -This is the **final** branch: the demo app with the ShieldLabs integration. The **starter** branch has the same app without protection. See what the integration adds with `git diff starter final -- web-scraping`. +## Run this application -## Setup +Use Node.js 22 or later. From this folder: -1. Install the dependencies (Node.js 20 or later): - - ```bash - npm install - ``` - -2. Copy `.env.example` to `.env` and add your ShieldLabs keys from **Integration > API keys**: the Public Key as `SHIELDLABS_PUBLIC_KEY` and the Private API Key (`sec_...`) as `SHIELDLABS_API_KEY`. -3. Start the server: - - ```bash - npm run dev - ``` - -4. Open the app on your development domain (see below). - -The flights and prices are made up for the demo. - -## Run it on your domain - -The ShieldLabs snippet only runs on a domain you added and verified in your ShieldLabs account: the Public Key is bound to that domain, so `localhost` and raw IP addresses are rejected. The starter branch runs on localhost as is. This branch needs your domain: +```sh +npm ci --omit=dev +cp .env.example .env +npm run dev +``` -1. In the [dashboard](https://app.shieldlabs.ai/), register a development domain such as `tutorial.your-domain.com` as its own domain under **Integration > Domains**, rather than reusing your production keys. No account yet? [Start Free](https://app.shieldlabs.ai/): 5,000 identifications one time, no credit card. -2. Copy that domain's keys into `.env` (see Setup). -3. Tunnel the domain to `localhost:3000`, for example with Cloudflare Tunnel, and open `https://tutorial.your-domain.com`. The [root README](../README.md#the-domain-requirement) has the commands. +The starter runs at http://127.0.0.1:3000 and needs no keys. The final needs a registered HTTPS hostname and the matching Public Key and Private API Key from **Integration > API keys**. Put them in `SHIELDLABS_PUBLIC_KEY` and `SHIELDLABS_API_KEY` in .env. The Private API Key stays on the server. Use your existing deployment/reverse proxy: no hosting provider is required. The service does not automatically accept a customer's key on localhost. -See [Environments](https://docs.shieldlabs.ai/setup/environments) and [Domains](https://docs.shieldlabs.ai/setup/domains) for the details. +## Compare starter and final -## How it works +Stop the server before switching versions. From the repository root run `git diff starter final -- web-scraping`, then switch branches and reinstall dependencies in this folder. Preserve your own uncommitted work in another clone rather than discarding it. Ignored .env files stay local; fill the final settings when moving from starter. Schema differences may recreate this app's disposable database. -- `public/shieldlabs.js` loads the ShieldLabs snippet and runs `forceCheckAnonymous` for every search. The request ID of that identification goes to the server with the search. -- `server/shieldlabs.js` reads the identification from the History API. The search returns no data when the identification is missing, unverified, rate limited, older than 5 minutes or already used, when it shows browser automation or disabled JavaScript, or when its Risk Score is in the Dangerous band (60-100). -- `server/flights.js` runs that check before anything else, so direct calls to `/api/flights` without an identification get no prices either. +In final, `public/shieldlabs.js` uses `@shieldlabs-ai/js@1.0.1` and sends a fresh action request ID. `server/shieldlabs.js` retrieves History with `@shieldlabs-ai/node@1.0.1`, rejects missing, stale, reused, limited and unusable results and applies the configured risk guard. It rereads after an 11-second observation delay as a demo precaution, not a server-guaranteed finality marker. The other modules in `server/` implement this app's rule and its own SQLite state. No sibling folder or root shared server is required. ## Try it -1. Search for flights in your browser. The table fills with flights and prices. -2. Call the API directly, the way a scraper would: - - ```bash - curl -s -X POST https://tutorial.your-domain.com/api/flights \ - -H 'Content-Type: application/json' \ - -d '{"from":"JFK","to":"LAX","date":"2026-12-01"}' - ``` +Search a made-up route in a normal browser, then inspect the automated test: an automation result returns no flights. The flight schedule is illustrative data. - The response has no flights: the request carries no identification. +Only use invented information and provided demo credentials/sample cards. Payments, orders, challenges, messages and rewards are simulated. This exercise is not a real loan decision or a transfer of funds. Keep live identifications over a minute apart and stop on rate limiting. Compare actual History Device IDs before interpreting private-window or changed-cookie behavior. -## Run the bot test +## Verify -With the server and the tunnel running, search from headless Chrome: - -```bash -BASE_URL=https://tutorial.your-domain.com node test-bot.js +```sh +npm run check +npm test ``` -The search returns no flights: headless Chrome raises the Browser Automation signal. +Tests use an isolated database and synthetic History responses; they do not call real scoring. The final suite covers this scenario, real SDK normalization, replay/freshness/automation guards, unavailable History, late updates and reset during a pending read. These tests do not substitute for a live check with the registered hostname. -## Reset the demo database +The optional `test-bot.js` needs dev dependencies: install with `npm ci`. Run it only in a controlled browser-test environment. This branch's ordinary app and native tests work with `npm ci --omit=dev`. -Click **Reset demo DB** at the bottom of the page, or run: +## Reset and production boundary -```bash -npm run reset-db -``` +`DEMO_ALLOW_RESET=1` enables **Reset demo DB**; `npm run reset-db` clears this folder's teaching state. Do not keep important information in db.sqlite. Demonstration accounts, reset/moderator tools and admin screens are not production authorization. Add your own authentication, session/action binding, durable state and shared replay storage before adapting the app. The teaching thresholds are not ShieldLabs High-Risk Event thresholds. + +Guide: [Web scraping](https://docs.shieldlabs.ai/tutorials/automated-scraping) (the new guide stays local until its separate documentation release). diff --git a/web-scraping/package-lock.json b/web-scraping/package-lock.json index 96bb0a1..64f8606 100644 --- a/web-scraping/package-lock.json +++ b/web-scraping/package-lock.json @@ -10,38 +10,17 @@ "license": "MIT", "dependencies": { "@fastify/static": "^10.1.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", "fastify": "^5.12.5" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" }, "engines": { - "node": ">=20" - } - }, - "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/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": ">=22" } }, "node_modules/@fastify/accept-negotiator": { @@ -257,76 +236,50 @@ "license": "MIT" }, "node_modules/@puppeteer/browsers": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", - "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "dev": true, - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } } }, - "node_modules/@puppeteer/browsers/node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", - "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", - "dev": true, - "optional": true, - "dependencies": { - "undici-types": "~8.9.0" + "node_modules/@shieldlabs-ai/js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/js/-/js-1.0.1.tgz", + "integrity": "sha512-K4sbefNO1bZLivj2UPdx+A2A72U6OSTiSweJvKcepvX3VMs44B2f9o86/IB19sycR/+FJR5rkfJwjegPSuCKnQ==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "optional": true, - "dependencies": { - "@types/node": "*" + "node_modules/@shieldlabs-ai/node": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@shieldlabs-ai/node/-/node-1.0.1.tgz", + "integrity": "sha512-sRTzBsFZnv8layqPsoufbINwLNPH5e45rvQs6MQP5vS7wiJ1m7FJLoBhFrx3HOeKxau85yhtYUulttXbTs6Wng==", + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/abstract-logging": { @@ -335,15 +288,6 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -394,47 +338,31 @@ "license": "BSD-3-Clause" }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -464,20 +392,6 @@ "fastq": "^1.17.1" } }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "dev": true, - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -487,86 +401,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "dev": true, - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "dev": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "dev": true - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "dev": true, - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "dev": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -586,15 +420,6 @@ } ] }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-sqlite3": { "version": "12.11.1", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", @@ -661,124 +486,59 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "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/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, "peerDependencies": { "devtools-protocol": "*" } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "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==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=20" } }, - "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/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, - "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 - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/decompress-response": { @@ -803,20 +563,6 @@ "node": ">=4.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dev": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -843,10 +589,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1608973", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", - "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", - "dev": true + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/dotenv": { "version": "17.4.2", @@ -860,10 +607,11 @@ } }, "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 + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, "node_modules/end-of-stream": { "version": "1.4.5", @@ -873,29 +621,12 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -905,67 +636,6 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "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/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/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "dev": true, - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -974,26 +644,6 @@ "node": ">=6" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", @@ -1006,12 +656,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-json-stringify": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", @@ -1119,15 +763,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -1157,39 +792,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dev": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1231,32 +851,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1276,22 +870,6 @@ } ] }, - "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/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1302,64 +880,6 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, - "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "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/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/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1435,19 +955,17 @@ ], "license": "MIT" }, - "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/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/mime": { @@ -1509,33 +1027,29 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "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/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "3.96.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz", @@ -1564,68 +1078,6 @@ "wrappy": "1" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dev": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dev": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "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/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1651,18 +1103,6 @@ "node": "20 || >=22" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true - }, - "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/pino": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", @@ -1742,40 +1182,6 @@ ], "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -1786,42 +1192,43 @@ } }, "node_modules/puppeteer": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.43.1.tgz", - "integrity": "sha512-/FSOViCrqRdb1HDocpsM9Z1giA71gTQPUt3SpHGVRALKAy/rJr1fLFYZW9F23qPxqVxTHQnbh/5B5opJST3kAw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.12.0.tgz", + "integrity": "sha512-agM9cYcxbvmU17w9qtF17s7/Vv7Dhbyig6AWwIgvQQLR2dKZdPqNvRPanac7xH4iMPcNYOCfyCkwqCnR4nVOcA==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1608973", - "puppeteer-core": "24.43.1", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.12.0", "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.43.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", - "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.2", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1608973", + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.20.0" + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/quick-format-unescaped": { @@ -1866,15 +1273,6 @@ "node": ">= 12.13.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1884,15 +1282,6 @@ "node": ">=0.10.0" } }, - "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/ret": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", @@ -2049,44 +1438,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", - "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", - "dev": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -2096,16 +1447,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2123,17 +1464,6 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "dev": true, - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2143,29 +1473,36 @@ } }, "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==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { @@ -2202,24 +1539,6 @@ "node": ">=6" } }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "dev": true, - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/thread-stream": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", @@ -2255,12 +1574,6 @@ "node": ">=0.6" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2273,17 +1586,11 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", "dev": true, - "optional": true + "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -2291,28 +1598,48 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", - "dev": true + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/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==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2323,6 +1650,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2344,45 +1672,37 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/zod": { @@ -2390,6 +1710,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web-scraping/package.json b/web-scraping/package.json index 6c15b89..3ab5eaf 100644 --- a/web-scraping/package.json +++ b/web-scraping/package.json @@ -7,20 +7,24 @@ "type": "module", "main": "server/server.js", "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "dev": "node --watch server/server.js", "start": "node server/server.js", - "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"" + "reset-db": "node -e \"import('./server/db.js').then((db) => db.resetDb())\"", + "test": "node --test test/*.test.js", + "check": "node --check server/server.js && node --check public/index.js" }, "dependencies": { "@fastify/static": "^10.1.5", "better-sqlite3": "^12.11.1", "dotenv": "^17.4.2", - "fastify": "^5.12.5" + "fastify": "^5.12.5", + "@shieldlabs-ai/js": "1.0.1", + "@shieldlabs-ai/node": "1.0.1" }, "devDependencies": { - "puppeteer": "^24.43.1" + "puppeteer": "25.12.0" } } diff --git a/web-scraping/public/index.html b/web-scraping/public/index.html index c16831c..1c97c45 100644 --- a/web-scraping/public/index.html +++ b/web-scraping/public/index.html @@ -8,6 +8,7 @@
+

ShieldLabs tutorial: simulated actions only. Do not enter real passwords, payment details or personal information.

Demo Flights

Find a flight

@@ -59,6 +60,7 @@

Find a flight

+ diff --git a/web-scraping/public/shieldlabs.js b/web-scraping/public/shieldlabs.js index 22d827d..6be835e 100644 --- a/web-scraping/public/shieldlabs.js +++ b/web-scraping/public/shieldlabs.js @@ -1,54 +1,52 @@ -// Browser side of the ShieldLabs integration. -// -// Loads the ShieldLabs snippet with your Public Key (served by /config.js) and runs -// one identification per protected action. The snippet only runs on the domain you -// added in the ShieldLabs dashboard, not on localhost: see the README. - -const snippet = import( - `https://cdn.shieldlabs.ai/snippet.js?publicKey=${encodeURIComponent(window.SHIELDLABS_PUBLIC_KEY ?? '')}` -).catch((error) => { - console.error('The ShieldLabs snippet did not load.', error); - return null; -}); +// The pinned browser SDK is served locally by this app, not by a sibling folder. +// Only request IDs go to the backend. No Private API Key or risk result is here. +if (window.SHIELDLABS_CONFIGURED === false) { + const notice = document.createElement('aside'); + notice.id = 'shieldlabs-setup-notice'; + notice.setAttribute('role', 'note'); + notice.style.cssText = 'padding:12px;margin:12px;border:1px solid #cc8c00;background:#fff6d8;color:#402d00'; + notice.textContent = 'Final integration is not configured. Add this domain’s Public Key and Private API Key to your private .env, restart the server, then reload. Follow this app’s README. Never put the Private API Key in browser code.'; + document.querySelector('main')?.prepend(notice); +} +let agentPromise; +let queue = Promise.resolve(); +let signedInUserHid; -// Runs a fresh identification and resolves with its request ID, or with null when -// no check ran. The browser never sees a Risk Score: the server reads the result. -// forceCheckAnonymous runs even inside the snippet's five-minute window, so every -// action gets its own identification. For an action by a signed-in user, call -// forceCheckAuthenticatedUser(hashedUserId, { onInitialized }) instead, with a -// hashed account id, never a raw email or username. -export function identify() { - return new Promise((resolve) => { - // Give up after 10 seconds: the server then refuses the action as unverified. - const timer = setTimeout(() => resolve(null), 10_000); - const done = (requestId) => { - clearTimeout(timer); - resolve(requestId); - }; +function agent() { + if (window.SHIELDLABS_CONFIGURED === false) return Promise.reject(new Error('Demo keys are not configured')); + if (!agentPromise) { + agentPromise = Promise.resolve().then(() => window.ShieldLabsJS.load({ publicKey: window.SHIELDLABS_PUBLIC_KEY, timeout: 10_000 })); + agentPromise.catch(() => { agentPromise = undefined; }); + } + return agentPromise; +} - snippet.then((shieldlabs) => { - if (!shieldlabs) return done(null); - // Pass the callback as { onInitialized }: a bare function is ignored. - shieldlabs.forceCheckAnonymous({ - onInitialized: (result) => done(result.status === 'initialized' ? result.requestID : null), - }); - }); - }); +export function identify(userId) { + const task = queue.then(async () => { + const loaded = await agent(); + const result = await loaded.identify(userId ? { userId } : undefined); + return result.requestId; + }).catch(() => null); // null means unverified; the server must refuse it. + queue = task.then(() => undefined); + return task; } -// Starts the identification when the user begins the action (the first focus -// inside the form), so it is usually scored by the time the form is sent. -// take() returns the request ID for this submit. The server accepts each -// identification for one action only, so the next submit starts a new one. -export function identifyOnFirstFocus(form) { - let pending = null; - const start = () => (pending ??= identify()); - form.addEventListener('focusin', start); - return { - take() { - const requestId = start(); - pending = null; - return requestId; - }, +export function identifyOnFirstFocus(form, { signedIn = false } = {}) { + let pending; + let startedAt = 0; + const start = () => { + if (!pending || Date.now() - startedAt > 240_000) { + startedAt = Date.now(); + pending = identify(signedIn ? signedInUserHid : undefined); + } + return pending; }; + form.addEventListener('focusin', start); + return { take() { const result = start(); pending = undefined; return result; } }; +} + +// Record the server-computed account HID for the next signed-in action, rather +// than launching an extra background check immediately after every signup. +export function checkSignedInUser(userId) { + signedInUserHid = typeof userId === 'string' && userId ? userId : undefined; } diff --git a/web-scraping/server/db.js b/web-scraping/server/db.js index 206d6a4..8dc46b2 100644 --- a/web-scraping/server/db.js +++ b/web-scraping/server/db.js @@ -2,7 +2,7 @@ import Database from 'better-sqlite3'; import { fileURLToPath } from 'node:url'; // The demo data lives in db.sqlite, next to package.json. -export const db = new Database(fileURLToPath(new URL('../db.sqlite', import.meta.url))); +export const db = new Database(process.env.DEMO_DB_PATH || fileURLToPath(new URL('../db.sqlite', import.meta.url))); // Bump this when the tables change. A database with another version (for example // after you switch between the starter and final branches) is recreated. diff --git a/web-scraping/server/server.js b/web-scraping/server/server.js index 8a3fe75..ca917f4 100644 --- a/web-scraping/server/server.js +++ b/web-scraping/server/server.js @@ -1,5 +1,9 @@ import 'dotenv/config'; import Fastify from 'fastify'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { resetVerificationState } from './shieldlabs.js'; import fastifyStatic from '@fastify/static'; import { fileURLToPath } from 'node:url'; import { initDb, resetDb } from './db.js'; @@ -7,13 +11,17 @@ import { AIRPORTS, searchFlights } from './flights.js'; initDb(); -const app = Fastify(); +export const app = Fastify({ bodyLimit: 16_384 }); app.register(fastifyStatic, { root: fileURLToPath(new URL('../public', import.meta.url)) }); +const require = createRequire(import.meta.url); +const bundle = join(dirname(require.resolve('@shieldlabs-ai/js/package.json')), 'dist/shieldlabs.iife.js'); +app.get('/vendor/shieldlabs.js', async (_request, reply) => reply.type('application/javascript').send(await readFile(bundle))); + // Expose only the Public Key to the browser. The Private API Key stays on the server. app.get('/config.js', async (_request, reply) => { - reply.type('application/javascript'); - return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')};`); + reply.header('cache-control', 'no-store').type('application/javascript'); + return reply.send(`window.SHIELDLABS_PUBLIC_KEY = ${JSON.stringify(process.env.SHIELDLABS_PUBLIC_KEY ?? '')}; window.SHIELDLABS_CONFIGURED = ${Boolean(process.env.SHIELDLABS_PUBLIC_KEY && process.env.SHIELDLABS_API_KEY && process.env.SHIELDLABS_PUBLIC_KEY !== 'your-public-key' && process.env.SHIELDLABS_API_KEY !== 'sec_your_private_api_key')};`); }); app.get('/api/airports', async (_request, reply) => { @@ -28,16 +36,21 @@ app.post('/api/flights', async (request, reply) => { // Reset the demo database. app.post('/api/reset-db', async (_request, reply) => { + if (process.env.DEMO_ALLOW_RESET !== '1') return reply.code(403).send({ success: false, message: 'Demo reset is disabled.' }); + resetVerificationState(); resetDb(); return reply.send({ success: true, message: 'Demo database reset.' }); }); // Show server errors in the response, to make the tutorial easy to debug. app.setErrorHandler((error, _request, reply) => { - console.error(error); - reply.status(500).send({ success: false, message: `Server error: ${error.message}` }); + console.warn('Demo request failed:', error.name); + reply.status(500).send({ success: false, message: 'The demo request could not be completed.' }); }); -const port = Number(process.env.PORT) || 3000; -await app.listen({ port }); -console.log(`Server running at http://localhost:${port}`); +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const port = Number(process.env.PORT) || 3000; + await app.listen({ port, host: '127.0.0.1' }); + console.log(`Server running at http://127.0.0.1:${port}`); + for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => app.close().catch(() => { process.exitCode = 1; })); +} diff --git a/web-scraping/server/shieldlabs.js b/web-scraping/server/shieldlabs.js index 791eb66..e577332 100644 --- a/web-scraping/server/shieldlabs.js +++ b/web-scraping/server/shieldlabs.js @@ -1,180 +1,84 @@ -// Server side of the ShieldLabs integration. -// -// The browser sends the request ID of the identification it ran for an action. -// This module reads that identification from the History API with your Private -// API Key and runs the checks every protected action starts with. +// This file belongs to this app: copying this folder is sufficient to run it. +// Public Key is browser-only; Private API Key stays in the server environment. +import { ShieldLabs, evaluateIdentification, riskBand } from '@shieldlabs-ai/node'; import { db } from './db.js'; -const API_URL = process.env.SHIELDLABS_API_URL || 'https://account.shieldlabs.ai'; -const API_KEY = process.env.SHIELDLABS_API_KEY; // sec_... Private API Key, never sent to the browser - export const NIL_DEVICE = '00000000-0000-0000-0000-000000000000'; -const POLL_INTERVAL_MS = 500; -const MAX_AGE_MS = 5 * 60 * 1000; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +let client; +let generation = 0; + +export function resetVerificationState() { generation += 1; } + +function historyClient() { + if (!process.env.SHIELDLABS_API_KEY) return null; + client ??= new ShieldLabs({ + apiKey: process.env.SHIELDLABS_API_KEY, + baseUrl: process.env.SHIELDLABS_API_URL, + timeout: 5_000, + maxRetries: 0, + fetch: (...args) => globalThis.fetch(...args), + }); + return client; +} -// Risk Score bands: Trusted 0-29, Suspicious 30-59, Dangerous 60-100. export function band(score) { - if (score >= 60) return 'Dangerous'; - if (score >= 30) return 'Suspicious'; - return 'Trusted'; + const value = riskBand(score); + return value === 'rate_limited' ? 'Rate limited' : value[0].toUpperCase() + value.slice(1); } -// The checks every protected handler starts with. Resolves with -// { ok: true, identification } or { ok: false, reason, message }. -// The messages name the reason to make the demo easy to follow. In production, -// show a generic message and log the reason. -export async function verifyIdentification(requestId) { - if (typeof requestId !== 'string' || !/^[\w-]{8,64}$/.test(requestId)) { - return refuse(requestId, 'missing', 'No identification was sent with the request.'); +export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return null; + try { + const sdk = historyClient(); + if (!sdk) return null; + const signal = AbortSignal.timeout(timeoutMs + 13_000); + const first = await sdk.identifications.get(requestId, { timeout: timeoutMs, signal }); + if (!first || first.request_id !== requestId) return null; + const observed = Date.parse(first.observed_at ?? ''); + if (!Number.isFinite(observed) || observed > Date.now() + 2_000) return null; + // Conservative demo delay, NOT a finality marker supplied by the server. + const remaining = Math.max(0, observed + 11_000 - Date.now()); + if (remaining) await new Promise(resolve => setTimeout(resolve, remaining)); + const row = await sdk.identifications.get(requestId, { wait: false, signal }); + const updated = Date.parse(row?.observed_at ?? ''); + if (!Number.isFinite(updated) || updated > Date.now() + 2_000) return null; + return row?.request_id === requestId ? row : null; + } catch (error) { + // Do not print credentials, transport URLs or arbitrary response bodies. + console.warn('[shieldlabs] Identification unavailable:', error.name); + return null; } +} - // No identification means unverified, never clean. +export async function verifyIdentification(requestId, { expectedUserHid, blockBands = ['dangerous'] } = {}) { + if (typeof requestId !== 'string' || !UUID.test(requestId)) return refuse('missing'); + const before = generation; const identification = await getIdentification(requestId); - if (!identification) { - return refuse(requestId, 'unverified', 'The identification could not be verified.'); - } - - // One identification authorizes one action, within 5 minutes of the check. - const { changes } = db - .prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)') - .run(requestId, Date.now()); - if (changes === 0) { - return refuse(requestId, 'replayed', 'This identification was already used.'); - } - if (!(Date.now() - parseTimestamp(identification.created_at) <= MAX_AGE_MS)) { - return refuse(requestId, 'expired', 'The identification is more than 5 minutes old.'); - } - - // 999 is the rate-limit marker. The all-zero Device ID means no usable device signals. - if (identification.risk_score > 100) { - return refuse(requestId, 'rate_limited', 'The identification was rate limited.'); - } - if (identification.device_id === NIL_DEVICE) { - return refuse(requestId, 'no_device', 'No usable device signals were collected.'); - } - - const flags = identification.detection_flags; - if (flags.browser_automation || flags.javascript_disabled) { - const what = flags.browser_automation ? 'Browser automation' : 'Disabled JavaScript'; - return refuse(requestId, 'automation', `${what} detected.`); - } - if (band(identification.risk_score) === 'Dangerous') { - const signals = identification.score_details.map((s) => `${s.description} ${s.weight}`).join(', '); - return refuse(requestId, 'dangerous', `Risk Score ${identification.risk_score} is in the Dangerous band.`, signals); - } - - console.log( - `[shieldlabs] ${requestId}: device ${identification.device_id}, ` + - `Risk Score ${identification.risk_score} (${band(identification.risk_score)})`, - ); + if (before !== generation) return refuse('reset_during_check'); + if (!identification) return refuse('unverified'); + if (expectedUserHid !== undefined && identification.user_hid !== expectedUserHid) return refuse('wrong_account'); + // Old IDs cannot pass freshness, so expired replay records can be removed. + db.prepare('DELETE FROM used_request_ids WHERE used_at < ?').run(Date.now() - 300_000); + const { changes } = db.prepare('INSERT OR IGNORE INTO used_request_ids (request_id, used_at) VALUES (?, ?)').run(requestId, Date.now()); + const verdict = evaluateIdentification(identification, { isReplay: () => changes === 0, blockBands }); + if (!verdict.ok) return refuse(verdict.reason); return { ok: true, identification }; } -// Reads one identification by request ID. Scoring is asynchronous, so the row can -// take a few seconds to appear (up to about 10 when follow-up network checks run): -// poll until it does. Returns the identification with webhook-style field names, -// or null. -export async function getIdentification(requestId, { timeoutMs = 10_000 } = {}) { - if (!API_KEY) { - console.error('[shieldlabs] SHIELDLABS_API_KEY is not set in .env: identifications cannot be read.'); - return null; - } - const deadline = Date.now() + timeoutMs; - while (true) { - try { - const [row] = await readHistory('request_id', requestId, 1); - if (row) return fromHistoryRow(row); - } catch (error) { - console.error(`[shieldlabs] History API: ${error.message}`); - if (error.status === 401 || error.status === 403) return null; // a wrong key does not heal - } - if (Date.now() + POLL_INTERVAL_MS > deadline) return null; - await sleep(POLL_INTERVAL_MS); - } -} - -// Reads identifications from the History API, newest first. searchType is one of -// request_id, device_id, visitor_id, user_hid, ip, session_id or cookie_id. export async function readHistory(searchType, value, limit = 20) { - if (!API_KEY) throw new Error('SHIELDLABS_API_KEY is not set in .env'); - - const url = new URL(`/api/v1/history/${searchType}/${encodeURIComponent(value)}`, API_URL); - url.searchParams.set('limit', String(limit)); - const response = await fetch(url, { - headers: { Authorization: `Bearer ${API_KEY}` }, - signal: AbortSignal.timeout(5_000), - }); - if (!response.ok) { - const error = new Error(`history lookup failed with HTTP ${response.status}`); - error.status = response.status; - throw error; - } - const body = await response.json(); - return body.data ?? []; + const sdk = historyClient(); + if (!sdk) throw new Error('SHIELDLABS_API_KEY is not configured'); + return (await sdk.history.search(searchType, value, { limit, signal: AbortSignal.timeout(10_000) })).data; } -// --- Helpers --- - -// History rows are flat and snake_case. Map the fields the tutorials read to the -// names the webhook payload uses. -function fromHistoryRow(row) { - return { - request_id: row.request_id, - session_id: row.session_id, - visitor_id: row.visitor_id, - device_id: row.device_id, - user_hid: row.user_hid, - risk_score: row.score, - score_details: parseScoreDetails(row.score_details), - public_ip: { ip: row.ip, country: row.country }, - connection_type: row.connection_type, - os: row.os, - browser: row.browser, - device_type: row.device_type, - created_at: row.created_at, - detection_flags: { - vpn: row.is_vpn, - proxy: row.is_proxy, - tor: row.is_tor, - privacy_relay: row.is_privacy_relay, - datacenter_ip: row.is_datacenter, - abuser: row.is_abuser, - anti_detect_browser: row.is_antidetect, - browser_automation: row.is_browser_automation, - javascript_disabled: row.is_js_disabled, - os_mismatch: row.is_os_mismatch, - os_not_detected: row.is_os_not_detected, - timezone_mismatch: row.is_timezone_mismatch, - incognito: row.is_incognito, - search_bot: row.is_search_bot, - }, +function refuse(reason) { + const messages = { + missing: 'No valid request ID was sent.', unverified: 'The identification could not be verified.', + replayed: 'This identification was already used.', stale: 'This identification is more than five minutes old.', + rate_limited: 'The identification was rate limited.', no_device_signals: 'No usable device signals were collected.', + blocked_flag: 'Automation or disabled JavaScript detected.', blocked_band: 'Risk Score is in the Dangerous band.', + wrong_account: 'This identification belongs to another account.', reset_during_check: 'The demo was reset during verification.', }; -} - -// score_details is a JSON string of { Value, Description } entries: the weight of -// each risk signal behind the Risk Score. Entries with Value 0 are notes, not signals. -function parseScoreDetails(value) { - try { - return JSON.parse(value || '[]') - .filter((entry) => entry.Value > 0) - .map((entry) => ({ weight: entry.Value, description: entry.Description })); - } catch { - return []; - } -} - -// History timestamps are UTC, for example "2026-06-16 10:00:00.000". -function parseTimestamp(value) { - const text = String(value ?? '').trim().replace(' ', 'T'); - return Date.parse(/Z$|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`); -} - -function refuse(requestId, reason, message, detail = '') { - const label = typeof requestId === 'string' && requestId ? requestId : 'no request ID'; - console.log(`[shieldlabs] ${label}: refused (${reason}) ${detail}`.trim()); - return { ok: false, reason, message }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return { ok: false, reason, message: messages[reason] ?? 'Another verification step is required.' }; } diff --git a/web-scraping/test/app.test.js b/web-scraping/test/app.test.js new file mode 100644 index 0000000..870a32e --- /dev/null +++ b/web-scraping/test/app.test.js @@ -0,0 +1,138 @@ +import test, { beforeEach, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.DEMO_DB_PATH = ':memory:'; +process.env.DEMO_ALLOW_RESET = '1'; +process.env.SHIELDLABS_PUBLIC_KEY = 'synthetic-public-key'; +process.env.SHIELDLABS_API_KEY = 'sec_abcd1234-efgh5678-ijkl9012'; +process.env.SHIELDLABS_API_URL = 'https://history.example.test'; +const DEVICE_A = '11111111-1111-4111-8111-111111111111'; +const DEVICE_B = '22222222-2222-4222-8222-222222222222'; +const [route, payload] = ["/api/flights",{"from":"JFK","to":"LAX","date":"2026-12-10"}]; +const originalFetch = globalThis.fetch; +let wire; +let calls = []; +let responseStatus = 200; +let transform; +let pausedFetch; +let markStarted; +globalThis.fetch = async (url, options) => { + calls.push({ url: String(url), options }); + assert.ok(String(url).startsWith('https://history.example.test/api/v1/history/')); + assert.equal(new Headers(options.headers).get('authorization'), 'Bearer ' + process.env.SHIELDLABS_API_KEY); + if (pausedFetch) { markStarted?.(); await pausedFetch; } + const row = transform ? transform({ ...wire }, calls.length) : wire; + return Response.json({ data: [row], total: 1 }, { status: responseStatus }); +}; +const { app } = await import('../server/server.js'); +const { db } = await import('../server/db.js'); +const { verifyIdentification } = await import('../server/shieldlabs.js'); + +function fixture(id, options = {}) { + return { + request_id: id, device_id: options.device ?? DEVICE_A, visitor_id: DEVICE_A, + session_id: DEVICE_A, cookie_id: DEVICE_A, user_hid: options.userHid ?? 'anonymous', + domain: 'tutorial.example.test', site_domain: 'example.test', ip: '203.0.113.24', + country: options.country ?? 'US', score: options.score ?? 15, + created_at: new Date(Date.now() - (options.age ?? 60_000)).toISOString(), + score_details: '[{"Value":15,"Description":"Is VPN"}]', + is_browser_automation: options.automation ?? false, is_js_disabled: false, + is_vpn: options.vpn ?? false, connection_type: options.vpn ? 'vpn' : 'direct', + }; +} +async function actAt(url, body = {}, options = {}, cookie) { + const requestId = options.id ?? randomUUID(); + wire = fixture(requestId, options); + const response = await app.inject({ method: 'POST', url, payload: { ...body, requestId }, headers: cookie ? { cookie } : {} }); + return { body: response.json(), cookie: response.cookies.map(item => item.name + '=' + item.value).join('; '), response, requestId }; +} +const act = (fields = {}, options = {}, cookie) => actAt(route, { ...payload, ...fields }, options, cookie); + +test('a History row belonging to another request cannot authorize an action', async () => { + transform = row => ({ ...row, request_id: randomUUID() }); + assert.equal((await act()).body.success, false); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); +test('simultaneous uses of one request ID authorize at most one action', async () => { + const id = randomUUID(); + const results = await Promise.all([act({}, { id }), act({}, { id })]); + assert.equal(results.filter(result => result.body.success).length, 1); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids WHERE request_id = ?').get(id).n, 1); +}); + +beforeEach(async () => { await app.inject({ method: 'POST', url: '/api/reset-db' }); calls = []; responseStatus = 200; transform = undefined; pausedFetch = undefined; markStarted = undefined; }); +after(async () => { await app.close(); db.close(); globalThis.fetch = originalFetch; }); + +test('web-scraping: serves its own page, public config and installed SDK', async () => { + const page = await app.inject({ url: '/' }); assert.equal(page.statusCode, 200); + assert.match(page.body, /vendor\/shieldlabs.js/); + const config = await app.inject({ url: '/config.js' }); assert.equal(config.statusCode, 200); + assert.ok(!config.body.includes(process.env.SHIELDLABS_API_KEY)); + const bundle = await app.inject({ url: '/vendor/shieldlabs.js' }); assert.equal(bundle.statusCode, 200); + assert.match(bundle.body, /ShieldLabsJS/); +}); +test('ordinary verified action is handled with the published Node SDK', async () => { + assert.equal((await act()).body.success, true); assert.equal(calls.length, 2); +}); +test('missing identification does not change protected state', async () => { + const response = await app.inject({ method: 'POST', url: route, payload }); + assert.equal(response.json().success, false); assert.equal(calls.length, 0); +}); +test('reusing a valid request ID is refused atomically', async () => { + const first = await act(); assert.equal(first.body.success, true); + assert.equal((await act({}, { id: first.requestId })).body.success, false); +}); +for (const [name, options] of [ + ['Dangerous', { score: 80 }], ['automation', { automation: true }], + ['rate-limit marker', { score: 999 }], ['missing device', { device: '00000000-0000-0000-0000-000000000000' }], + ['stale result', { age: 360_000 }], ['future timestamp', { age: -60_000 }], +]) test(name + ' cannot authorize a normal protected action', async () => { + assert.equal((await act({}, options)).body.success, false); +}); +test('a named result for a different account is refused', async () => { + const id = randomUUID(); wire = fixture(id, { userHid: 'other' }); + const result = await verifyIdentification(id, { expectedUserHid: 'intended' }); + assert.equal(result.reason, 'wrong_account'); +}); +test('reset is disabled outside explicitly disposable mode', async () => { + process.env.DEMO_ALLOW_RESET = '0'; + try { assert.equal((await app.inject({ method: 'POST', url: '/api/reset-db' })).statusCode, 403); } + finally { process.env.DEMO_ALLOW_RESET = '1'; } +}); + +test('automation receives no flight data', async () => { + assert.ok((await act()).body.flights.length > 0); + assert.deepEqual((await act({}, { automation: true })).body.flights, []); +}); + +test('an unavailable History API never turns into a synthetic success', async () => { + responseStatus = 401; + assert.equal((await act()).body.success, false); + assert.equal(calls.length, 1); +}); +test('the delayed reread determines risk, not the early low score', async () => { + transform = (row, count) => ({ ...row, score: count === 1 ? 15 : 80 }); + const result = await act(); + assert.equal(result.body.success, false); + + assert.equal(calls.length, 2); +}); +test('a future timestamp on the second read is also refused', async () => { + transform = (row, count) => count === 1 ? row : { ...row, created_at: new Date(Date.now() + 60_000).toISOString() }; + assert.equal((await act()).body.success, false); +}); +test('reset while History is pending cannot repopulate cleared verification state', async () => { + let release, announce; + pausedFetch = new Promise(resolve => { release = resolve; }); + const started = new Promise(resolve => { announce = resolve; }); + markStarted = announce; + const pending = act(); + await started; + await app.inject({ method: 'POST', url: '/api/reset-db' }); + pausedFetch = undefined; release(); + const result = await pending; + assert.equal(result.body.success, false); + assert.match(result.body.message, /reset/); + assert.equal(db.prepare('SELECT COUNT(*) AS n FROM used_request_ids').get().n, 0); +}); diff --git a/web-scraping/test/browser.test.js b/web-scraping/test/browser.test.js new file mode 100644 index 0000000..b2f18a4 --- /dev/null +++ b/web-scraping/test/browser.test.js @@ -0,0 +1,51 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +const source = await readFile(new URL('../public/shieldlabs.js', import.meta.url), 'utf8'); +let imports = 0; +async function browser(mock) { + const previous = globalThis.window; + globalThis.window = { SHIELDLABS_PUBLIC_KEY: 'synthetic-key', ShieldLabsJS: { load: async () => mock } }; + const code = source.replaceAll('export ', ''); + const mod = new Function(code + '\nreturn { identify, identifyOnFirstFocus, checkSignedInUser };')(); + imports += 1; + return { ...mod, restore() { globalThis.window = previous; } }; +} +test('the browser SDK returns request IDs and serializes simultaneous action checks', async () => { + let active = 0, maximum = 0, count = 0; + const mod = await browser({ identify: async () => { + active++; maximum = Math.max(maximum, active); await new Promise(resolve => setTimeout(resolve, 2)); active--; + return { requestId: 'request-' + (++count) }; + } }); + try { assert.deepEqual(await Promise.all([mod.identify(), mod.identify()]), ['request-1','request-2']); assert.equal(maximum, 1); } + finally { mod.restore(); } +}); +test('first focus shares its pending check with submit; another submit gets a fresh ID', async () => { + let count = 0; + const mod = await browser({ identify: async () => ({ requestId: String(++count) }) }); + try { + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form); + form.dispatchEvent(new Event('focusin')); + assert.equal(await handle.take(), '1'); assert.equal(await handle.take(), '2'); + } finally { mod.restore(); } +}); +test('a failed agent returns no ID, never a made-up successful result', async () => { + const mod = await browser({ identify: async () => { throw new Error('synthetic timeout'); } }); + try { assert.equal(await mod.identify(), null); } finally { mod.restore(); } +}); +test('signed-in action uses its account HID without an extra background identification', async () => { + const calls = []; + const mod = await browser({ identify: async options => { calls.push(options); return { requestId: 'id' }; } }); + try { + mod.checkSignedInUser('opaque-account'); assert.equal(calls.length, 0); + const form = new EventTarget(); const handle = mod.identifyOnFirstFocus(form, { signedIn: true }); + assert.equal(await handle.take(), 'id'); assert.deepEqual(calls, [{ userId: 'opaque-account' }]); + } finally { mod.restore(); } +}); + +test('unconfigured final does not call the scoring agent', async () => { + let attempts=0; + const mod=await browser({identify:async()=>{attempts++;return{requestId:'should-not-run'};}}); + try { window.SHIELDLABS_CONFIGURED=false; assert.equal(await mod.identify(),null); assert.equal(attempts,0); } + finally { mod.restore(); } +});