diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index dad17d1..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,65 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: cimg/node:14.17.0 - environment: - AWS_PAGER: '' - steps: - - checkout - - run: - name: Install AWS CLI v2 - command: | - curl -sSL https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o awscliv2.zip - unzip awscliv2.zip - rm awscliv2.zip - sudo aws/install - - restore_cache: - keys: - - node14-dependencies-v2-{{ checksum "yarn.lock" }} - - node14-dependencies-v2- - - run: - name: Install Yarn - command: | - # Remove preinstalled Yarn to avoid conflicts - sudo rm -f /usr/local/bin/yarn - # Get Yarn version from package.json and remove >= prefix - YARN_VERSION=$( jq -r '(.engines.yarn | gsub("[^0-9.]";""))' < package.json ) - # Download and install - curl -sSL "https://github.com/yarnpkg/yarn/releases/download/v$YARN_VERSION/yarn_${YARN_VERSION}_all.deb" -o yarn.deb - sudo dpkg -i yarn.deb - rm yarn.deb - - run: - name: Install dependencies - command: yarn --frozen-lockfile - - save_cache: - paths: - - ~/.cache/yarn - - node_modules - key: node14-dependencies-v2-{{ checksum "yarn.lock" }} - - run: - name: Build - command: | - export REACT_APP_VERSION=${CIRCLE_SHA1:0:7} - export REACT_APP_BRANCH=$CIRCLE_BRANCH - export REACT_APP_BUILD_DATE=$(TZ=GMT date +'%b %_d, %Y, %_I:%M %p %Z') - yarn run build - - run: - name: Test - command: yarn run test - - deploy: - name: Deploy if allowed - command: | - BRANCHES='master candidate release' - if [[ " $BRANCHES " = *" $CIRCLE_BRANCH "* ]]; then - BUCKET=vasttester-origin-$CIRCLE_BRANCH.iabtechlab.com - echo Deploying to $BUCKET... - aws s3 sync \ - build/ \ - s3://$BUCKET/ \ - --delete \ - --cache-control max-age=300 \ - --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers - else - echo Not deploying $CIRCLE_BRANCH branch - fi diff --git a/.env b/.env deleted file mode 100644 index 84c467e..0000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -REACT_APP_VENDOR=IAB Tech Lab -REACT_APP_NAME=VAST Tester diff --git a/.env.development b/.env.development deleted file mode 100644 index 43a8af1..0000000 --- a/.env.development +++ /dev/null @@ -1,4 +0,0 @@ -REACT_APP_VERSION=0a1b2c3 -REACT_APP_BRANCH=master -REACT_APP_BUILD_DATE=Jan 1, 2021, 0:00 AM GMT -DISABLE_ESLINT_PLUGIN=true diff --git a/.env.production b/.env.production deleted file mode 100644 index e69de29..0000000 diff --git a/.gitignore b/.gitignore index d11b48c..71a089d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,13 @@ # production /build +/dist # misc .DS_Store +.env +.env.* +!.env.example .env.local .env.development.local .env.test.local @@ -22,3 +26,8 @@ yarn-error.log* .envrc .idea/ + +# TypeScript / Vite build output +*.tsbuildinfo +/vite.config.js +/vite.config.d.ts diff --git a/README.md b/README.md index 9c8ab77..8be8591 100644 --- a/README.md +++ b/README.md @@ -1,94 +1,102 @@ -Please review the IAB Tech Lab Open Source Initiative Governance guidelines [here](http://iabtechlab.com/opensource) for contributing to this project. +# VAST Tester AleksUIX -# IAB Tech Lab VAST Tester +A modern, vastlint-powered VAST validator, debugger, and QA workbench. -[![CircleCI](https://circleci.com/gh/InteractiveAdvertisingBureau/VAST-Tester.svg?style=shield)](https://circleci.com/gh/InteractiveAdvertisingBureau/VAST-Tester) [![JavaScript Standard Style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](https://standardjs.com/) +If you are looking for a new and improved alternative to the IAB Tech Lab VAST Tester, this repository is meant to be that starting point. It keeps the familiar idea of a browser-based VAST tester, but adds deeper validation, wrapper inspection, playback diagnostics, tracking visibility, and shareable findings for real ad-tech workflows. -Tests IAB VAST ads. Contributed by the [DoubleVerify](https://www.doubleverify.com/) team. +This project is built on top of [`vastlint`](https://github.com/aleksUIX/vastlint), the Rust-based VAST validation engine behind [vastlint.org](https://vastlint.org). It is an independent open-source project and is not affiliated with or endorsed by IAB Tech Lab. -This tool is also hosted by IAB Tech Lab at [[vasttester.iabtechlab.com](https://tools.iabtechlab.com/resourcecenter/vastTagValidator)]([https://vasttester.iabtechlab.com](https://tools.iabtechlab.com/resourcecenter/vastTagValidator)). +## Looking for the IAB Tech Lab VAST Tester? + +If that search term brought you here, the short version is this: this repo is an open-source, modernized VAST tester built for teams who want more than a legacy pass/fail checker. + +It is designed for buyers, sellers, SSPs, DSPs, SSAI teams, QA engineers, and ad-ops workflows that need to: + +- validate VAST XML against the IAB VAST specification +- inspect wrapper chains and resolved ads in detail +- understand tracking, macro expansion, and playback behavior +- share findings quickly with partners, vendors, or internal engineering teams + +## What This Repo Does + +- Validate pasted VAST XML or a remote VAST URL +- Auto-fix deterministic issues that `vastlint` can repair +- Resolve wrapper chains through `vastlint-client` +- Inspect wrapper hops, resolved ads, media files, and rule findings in one UI +- Display findings inline while editing XML +- Review playback-oriented runtime signals, tracking waterfalls, and macro previews +- Switch between different compliance-oriented validation profiles +- Export reports and copy error summaries for partner debugging + +## Built on `vastlint` + +This UI is the interactive frontend layer for the broader `vastlint` ecosystem. + +- Core project: [github.com/aleksUIX/vastlint](https://github.com/aleksUIX/vastlint) +- Hosted web validator: [vastlint.org](https://vastlint.org) +- npm packages: + - [npmjs.com/package/vastlint](https://www.npmjs.com/package/vastlint) + - [npmjs.com/package/vastlint-client](https://www.npmjs.com/package/vastlint-client) + - [npmjs.com/package/vastlint-react](https://www.npmjs.com/package/vastlint-react) + +Use this repo when you want a browser-first debugging workflow. Use `vastlint` directly when you want CLI automation, CI checks, MCP integration, or to embed VAST validation inside another system. ## Getting Started Install dependencies: ```bash -yarn +npm install ``` -Get developing: +Start the dev server: ```bash -yarn start +npm run dev ``` Create a production build: ```bash -yarn run build +npm run build ``` -## Architecture - -This is a [React](https://reactjs.org/) app bootstrapped with -[Create React App](https://github.com/facebookincubator/create-react-app). -All state is maintained using [Redux](https://redux.js.org/). Side effects of -state mutation are modeled using -[redux-observable](https://redux-observable.js.org/). +## Deploy -There are subdirectories for the standard React-Redux model: +This repo is intended to stay separate from `vastlint-infra` and deploy directly to Cloudflare Pages on the custom hostname `iab-tech-lab-vast-tester.vastlint.org`. -- [`components/`](src/components/): React components (without Redux); -- [`containers/`](src/containers/): React components connected to Redux's - store; -- [`actions/`](src/actions/): Redux action definitions; -- [`reducers/`](src/reducers/): Redux reducers; -- [`epics/`](src/epics/): epics for redux-observable; -- [`middleware/`](src/middleware/): Redux middleware. +One-time Cloudflare auth on your machine: -In addition to those, there are also: - -- [`util/`](src/util/): various utility modules; -- [`style/`](src/style): [Sass](https://sass-lang.com/) style sheets for - the app. - -More detailed documentation will be added at a later stage. For now, we suggest -exploring the source code. +```bash +npm run cf:login +``` -## Debugging +One-time Pages project creation: -During development, you can use: +```bash +npm run cf:pages:create +``` -- [React DevTools](https://github.com/facebook/react-devtools) - for React's DOM; -- [Redux DevTools](https://github.com/zalmoxisus/redux-devtools-extension) - for Redux actions and redux-observable effects; -- [Logger for Redux](https://github.com/evgenyrodionov/redux-logger) - by setting `localStorage.reduxLogger` to `true`; +Deploy the current branch from your local machine: -## To Do +```bash +npm run deploy:pages +``` -- OM SDK in-app support -- Resize support -- Canned test scenarios -- Reporting and recommendations -- VAST validation +After the first successful deploy, attach the custom domain in Cloudflare Pages: -## Contributing +```text +iab-tech-lab-vast-tester.vastlint.org +``` -We welcome pull requests for bug fixes and new features. +This workflow avoids GitHub Actions and repo secrets while keeping deployment repeatable from the local CLI. -## License +## Why This Exists -Copyright 2021 IAB Technology Laboratory, Inc. +The goal is not to reproduce the old tester one-to-one. The goal is to provide a stronger open-source workflow for anyone searching for an IAB Tech Lab VAST Tester style tool, but needing more visibility into why a tag fails, how wrappers resolve, what media is actually returned, and where tracking or compliance issues appear. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at -. +## Notes -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +- The app installs `vastlint`, `vastlint-client`, and `vastlint-react` directly from npm. +- For URL-backed validation, the target endpoint must allow browser-side fetching from your local dev or deployed origin. +- Browser playback results can vary based on codec support and remote asset permissions. diff --git a/index.html b/index.html new file mode 100644 index 0000000..aeaacc4 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + Next-Gen VAST Tester Fork + + +
+ + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..de42131 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2296 @@ +{ + "name": "vast-tester-aleksuix", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vast-tester-aleksuix", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "jszip": "^3.10.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "vastlint": "0.4.21", + "vastlint-client": "0.4.21", + "vastlint-react": "0.4.21" + }, + "devDependencies": { + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.9.3", + "vite": "^7.1.7", + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-wasm": "^3.5.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.40.tgz", + "integrity": "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.40", + "@swc/core-darwin-x64": "1.15.40", + "@swc/core-linux-arm-gnueabihf": "1.15.40", + "@swc/core-linux-arm64-gnu": "1.15.40", + "@swc/core-linux-arm64-musl": "1.15.40", + "@swc/core-linux-ppc64-gnu": "1.15.40", + "@swc/core-linux-s390x-gnu": "1.15.40", + "@swc/core-linux-x64-gnu": "1.15.40", + "@swc/core-linux-x64-musl": "1.15.40", + "@swc/core-win32-arm64-msvc": "1.15.40", + "@swc/core-win32-ia32-msvc": "1.15.40", + "@swc/core-win32-x64-msvc": "1.15.40" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz", + "integrity": "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz", + "integrity": "sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz", + "integrity": "sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz", + "integrity": "sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz", + "integrity": "sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz", + "integrity": "sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz", + "integrity": "sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz", + "integrity": "sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz", + "integrity": "sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz", + "integrity": "sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz", + "integrity": "sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz", + "integrity": "sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@swc/wasm": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.15.40.tgz", + "integrity": "sha512-FVS3SEJXBxjpxVUGSzaTaCdJjnXUalRftA/6hILMAJIcYHBoiBfJlxuH6s47iajlAJZP25e5Kf4HNHvvwyOEgw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz", + "integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "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/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "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/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "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/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vastlint": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/vastlint/-/vastlint-0.4.21.tgz", + "integrity": "sha512-+of4BnsxnczRMV66/XLmM1ohaf/S51qipNFIikvBqADiY/OPwRQGH0mtKpO1FJZ7fSFuR7ModFpNE0RpiFNqEQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/vastlint-client": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/vastlint-client/-/vastlint-client-0.4.21.tgz", + "integrity": "sha512-f5ZELBfnqzh2BfZzBy7pfMX7vKOkw+lfH5J2xgTcD0taxFMzt+78y7Mnch4999nEUIluQXIuoTjRP7B00zSKnQ==", + "license": "Apache-2.0", + "dependencies": { + "vastlint": "^0.4.21" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/vastlint-react": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/vastlint-react/-/vastlint-react-0.4.21.tgz", + "integrity": "sha512-1h/RaAYe2DTdNS3EGna5NEsTqt5IWVC+uaSaw6rC2rbiOK80BKJMWvN4xm+I401Lttoo2UPLlMs4Zq8j7nuXUg==", + "license": "Apache-2.0", + "dependencies": { + "vastlint-client": "^0.4.21" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.6.0.tgz", + "integrity": "sha512-bNhUreLamTIkoulCR9aDXbTbhLk6n1YE8NJUTTxl5RYskNRtzOR0ASzSjBVRtNdjIfngDXo11qOsybGLNsrdww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.12.14", + "@swc/wasm": "^1.12.14", + "uuid": "10.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz", + "integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json index c69091f..4d8d1a6 100644 --- a/package.json +++ b/package.json @@ -1,79 +1,36 @@ { - "name": "VAST-Tester", - "version": "0.0.0", - "description": "Tests IAB VAST ads.", + "name": "vast-tester-aleksuix", + "version": "0.1.0", + "description": "Modern VAST tester built on top of published vastlint packages.", "private": true, - "homepage": "https://vasttester.iabtechlab.com/", - "repository": "InteractiveAdvertisingBureau/VAST-Tester", + "type": "module", "license": "Apache-2.0", "engines": { - "node": ">=12", - "yarn": ">=1.22.10" + "node": ">=18" }, - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ], "scripts": { - "start": "react-scripts start", - "clean": "rimraf build", - "build": "yarn run clean && react-scripts build", - "test": "yarn run lint", - "lint": "eslint \"src/**/*.js\"", - "format": "prettier-eslint --write \"$PWD/src/**/*.js\"", - "prepare": "husky install" + "dev": "vite", + "build": "tsc -b && vite build", + "cf:login": "npx wrangler login", + "cf:pages:create": "npx wrangler pages project create iab-tech-lab-vast-tester --production-branch=master", + "deploy:pages": "npm run build && npx wrangler pages deploy dist --project-name=iab-tech-lab-vast-tester --branch=master", + "preview": "vite preview" }, "dependencies": { - "base16": "^1.0.0", - "date-fns": "^2.22.1", - "file-saver": "^2.0.5", - "font-awesome": "^4.7.0", - "iab-vast-loader": "^2.5.1", - "lodash-es": "^4.17.21", - "lower-case-first": "^2.0.2", - "prop-types": "^15.7.2", - "qs": "^6.10.1", - "react": "^17.0.2", - "react-collapsible": "^2.8.3", - "react-copy-to-clipboard": "^5.0.3", - "react-dom": "^17.0.2", - "react-fontawesome": "^1.6.1", - "react-json-tree": "^0.15.0", - "react-redux": "^7.2.4", - "react-router-dom": "^5.0.1", - "react-tabs": "^3.2.2", - "react-textarea-autosize": "^8.3.3", - "react-toggle": "^4.1.2", - "redux": "^4.1.0", - "redux-actions": "^2.6.5", - "redux-logger": "^3.0.6", - "redux-observable": "^2.0.0-rc.2", - "reset-css": "^5.0.1", - "rxjs": "^7.1.0", - "serialize-error": "^8.1.0", - "upper-case-first": "^2.0.2" + "jszip": "^3.10.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "vastlint": "0.4.21", + "vastlint-client": "0.4.21", + "vastlint-react": "0.4.21" }, "devDependencies": { - "eslint": "^7.28.0", - "eslint-config-standard": "^16.0.3", - "eslint-plugin-import": "^2.23.4", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-react": "^7.24.0", - "eslint-plugin-simple-import-sort": "^7.0.0", - "husky": "^6.0.0", - "lint-staged": "^11.0.0", - "node-sass": "^5.0.0", - "prettier-eslint-cli": "^5.0.1", - "react-scripts": "^4.0.3", - "redux-devtools-extension": "^2.13.9", - "rimraf": "^3.0.2" - }, - "lint-staged": { - "src/**/*.js": [ - "prettier-eslint --write" - ] + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.9.3", + "vite": "^7.1.7", + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-wasm": "^3.5.0" } } diff --git a/public/fixtures/vast/vast3-relative.xml b/public/fixtures/vast/vast3-relative.xml index 51b351d..6822654 100644 --- a/public/fixtures/vast/vast3-relative.xml +++ b/public/fixtures/vast/vast3-relative.xml @@ -19,7 +19,7 @@ 00:00:17.320 - + @@ -38,7 +38,7 @@ - + diff --git a/public/fixtures/vast/vast3.xml b/public/fixtures/vast/vast3.xml index c79682e..6822654 100644 --- a/public/fixtures/vast/vast3.xml +++ b/public/fixtures/vast/vast3.xml @@ -19,17 +19,17 @@ 00:00:17.320 - + - + - + - + @@ -38,9 +38,9 @@ - + - + diff --git a/public/fixtures/vast/vast4-relative.xml b/public/fixtures/vast/vast4-relative.xml index 9e2e623..4e88fec 100644 --- a/public/fixtures/vast/vast4-relative.xml +++ b/public/fixtures/vast/vast4-relative.xml @@ -19,7 +19,7 @@ 00:00:17.320 - + @@ -36,7 +36,7 @@ - + diff --git a/public/fixtures/vast/vast4.xml b/public/fixtures/vast/vast4.xml index 13ccb5f..4e88fec 100644 --- a/public/fixtures/vast/vast4.xml +++ b/public/fixtures/vast/vast4.xml @@ -19,26 +19,26 @@ 00:00:17.320 - + - + - + - + - + - + diff --git a/public/scenarios/wrapper-inline.xml b/public/scenarios/wrapper-inline.xml new file mode 100644 index 0000000..fb36103 --- /dev/null +++ b/public/scenarios/wrapper-inline.xml @@ -0,0 +1,32 @@ + + + + + vastlint-demo + wrapper scenario inline + + + + 8465 + + 00:00:15 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/scenarios/wrapper-root.xml b/public/scenarios/wrapper-root.xml new file mode 100644 index 0000000..1cc6591 --- /dev/null +++ b/public/scenarios/wrapper-root.xml @@ -0,0 +1,22 @@ + + + + + vastlint-demo + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..47802dd --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,3524 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import JSZip from "jszip"; +import { useVastPlayback, useVastSession, useVastTracker } from "vastlint-react"; +import adIdentityXml from "./scenarios/ad-identity.xml?raw"; +import adVerificationXml from "./scenarios/ad-verification.xml?raw"; +import brokenXml from "./scenarios/broken-tag.xml?raw"; +import clickTrackingXml from "./scenarios/click-tracking.xml?raw"; +import closedCaptionsXml from "./scenarios/closed-captions.xml?raw"; +import companionBannerXml from "./scenarios/companion-banner.xml?raw"; +import fixableXml from "./scenarios/fixable-tag.xml?raw"; +import iconFallbacksXml from "./scenarios/icon-fallbacks.xml?raw"; +import mezzanineSupportXml from "./scenarios/mezzanine-support.xml?raw"; +import nonLinearOverlayXml from "./scenarios/non-linear-overlay.xml?raw"; +import pricingCategoryXml from "./scenarios/pricing-category.xml?raw"; +import runtimeSurfacesXml from "./scenarios/runtime-surfaces.xml?raw"; +import sampleXml from "./scenarios/sample-inline.xml?raw"; +import skippableLinearXml from "./scenarios/skippable-linear.xml?raw"; +import viewableImpressionXml from "./scenarios/viewable-impression.xml?raw"; +import wrapperSignalsXml from "./scenarios/wrapper-signals.xml?raw"; + +import type { FixResult, Issue, ValidateOptions } from "vastlint"; +import { createVastSession } from "vastlint-client"; + +import type { + VastCompanionAd, + VastCreativeResource, + VastIcon, + VastPlaybackViewability, + VastResolvedAd, + VastSessionSnapshot, + VastSessionSource, + VastTrackingDispatchResult, + VastTrackingPlan, + VastTrackingTarget, + VastWrapperHop, +} from "vastlint-client"; + +type SourceMode = "xml" | "url"; +type ActionMode = "validate" | "resolve" | "fix"; +type ComplianceProfileId = "strict-iab" | "ctv-safe" | "ssai-safe" | "legacy-player"; +type ScenarioGroupId = "core" | "creative-types" | "measurement" | "ctv-ssai"; +type ScenarioActionFilter = "all" | ActionMode; + +interface RunRequest { + id: number; + sourceMode: SourceMode; + action: ActionMode; + payload: string; +} + +interface ScenarioPreset { + id: string; + label: string; + description: string; + groupId: ScenarioGroupId; + versionLabel: string; + focusAreas: readonly string[]; + sourceMode: SourceMode; + action: ActionMode; + payload: string; +} + +interface ScenarioGroupDefinition { + id: ScenarioGroupId; + label: string; + description: string; +} + +interface SharedSessionState { + sourceMode: SourceMode; + action: ActionMode; + payload: string; + activeScenarioId: string | null; + selectedComplianceProfileId: ComplianceProfileId | null; +} + +interface TimelineEntry { + id: string; + at: string; + title: string; + detail: string; + kind: "ui" | "media" | "session" | "tracking"; +} + +interface RuntimeVerificationResource { + id: string; + adTitle: string; + vendor: string; + kind: string; + apiFramework: string | null; + url: string; +} + +interface RuntimeCreativePreview { + id: string; + adTitle: string; + title: string; + resource: VastCreativeResource | null; + clickThroughUrl: string | null; +} + +interface RuntimeInspection { + apiFrameworks: string[]; + verificationResources: RuntimeVerificationResource[]; + companions: RuntimeCreativePreview[]; + icons: RuntimeCreativePreview[]; + omidCount: number; + vpaidCount: number; +} + +interface MacroEntryDraft { + id: string; + key: string; + value: string; +} + +interface MacroPresetDefinition { + id: string; + label: string; + description: string; + macros: Record; +} + +type TrackingWaterfallStatus = "ready" | "ok" | "failed" | "linked"; + +interface TrackingWaterfallRow { + id: string; + event: string; + kind: string; + status: TrackingWaterfallStatus; + originalUrl: string; + expandedUrl: string; + hopIndex: number; + sourceUrl: string | null; + offset: string | null; + dispatchCount: number; + lastDispatchedAt: string | null; + httpStatus: number | null; + error: string | null; +} + +type ComplianceProfileStatus = "pass" | "attention" | "fail"; + +interface ComplianceProfileVerdict { + id: ComplianceProfileId; + label: string; + description: string; + status: ComplianceProfileStatus; + summary: string; + reasons: string[]; +} + +type AssetRiskLevel = "ok" | "attention" | "risk"; + +interface AssetAuditRow { + id: string; + assetType: string; + adTitle: string; + format: string; + dimensions: string; + transport: string; + riskLevel: AssetRiskLevel; + riskLabel: string; + detail: string; + url: string | null; +} + +type HopInspectorTone = "ok" | "warning" | "error"; + +interface WrapperHopInspector { + id: string; + hopIndex: number; + title: string; + adType: string; + adSystem: string; + duration: string; + sourceLabel: string; + nextHopLabel: string; + fetchedAt: string; + fetchMs: number; + validationSummary: string; + tone: HopInspectorTone; + stats: string[]; + changes: string[]; +} + +interface EditorIssueMarker { + id: string; + line: number; + severity: Issue["severity"]; + issueCount: number; + summary: string; + title: string; + top: number; +} + +const EDITOR_LINE_HEIGHT = 28; +const EDITOR_VERTICAL_PADDING = 16; +const DEFAULT_APP_ORIGIN = "http://localhost:5175"; +const SCENARIO_FALLBACK_ASSET_ORIGIN = "https://iab-tech-lab-vast-tester.vastlint.org"; +const APP_BASE_PATH = import.meta.env.BASE_URL ?? "/"; + +const PROFILE_RULE_DEFAULT_SEVERITIES: Record = { + "VAST-2.0-flash-mediafile": "warning", + "VAST-2.0-mediafile-https": "warning", + "VAST-2.0-tracking-https": "warning", + "VAST-4.0-universaladid-present": "error", + "VAST-4.0-universaladid-idregistry": "error", + "VAST-4.0-universaladid-idvalue": "error", + "VAST-4.1-adservingid-present": "error", + "VAST-4.1-ad-serving-id-empty": "warning", + "VAST-4.1-universaladid-content": "error", + "VAST-4.1-universaladid-idvalue-removed": "warning", + "VAST-4.1-vpaid-apiframework": "warning", + "VAST-4.1-vpaid-in-interactive-context": "warning", + "VAST-4.1-mezzanine-recommended": "info", +}; + +const PROFILE_RULE_OVERRIDES: Record>> = { + "strict-iab": {}, + "ctv-safe": { + "VAST-2.0-flash-mediafile": "error", + "VAST-2.0-mediafile-https": "error", + "VAST-2.0-tracking-https": "error", + "VAST-4.1-vpaid-apiframework": "error", + "VAST-4.1-vpaid-in-interactive-context": "error", + "VAST-4.1-mezzanine-recommended": "error", + }, + "ssai-safe": { + "VAST-2.0-mediafile-https": "error", + "VAST-2.0-tracking-https": "error", + "VAST-4.1-ad-serving-id-empty": "error", + "VAST-4.1-vpaid-apiframework": "error", + "VAST-4.1-vpaid-in-interactive-context": "error", + "VAST-4.1-mezzanine-recommended": "error", + }, + "legacy-player": { + "VAST-4.0-universaladid-present": "warning", + "VAST-4.0-universaladid-idregistry": "warning", + "VAST-4.0-universaladid-idvalue": "warning", + "VAST-4.1-adservingid-present": "warning", + "VAST-4.1-universaladid-content": "warning", + "VAST-4.1-universaladid-idvalue-removed": "info", + "VAST-4.1-vpaid-apiframework": "info", + }, +}; + +function isComplianceProfileId(value: string | null | undefined): value is ComplianceProfileId { + return value === "strict-iab" || value === "ctv-safe" || value === "ssai-safe" || value === "legacy-player"; +} + +function buildComplianceValidateOptions(profileId: ComplianceProfileId): ValidateOptions | undefined { + const ruleOverrides = PROFILE_RULE_OVERRIDES[profileId]; + return Object.keys(ruleOverrides).length > 0 + ? { + rule_overrides: Object.fromEntries(Object.entries(ruleOverrides)) as Record, + } + : undefined; +} + +function getIssueSeverityForProfile(issue: Issue, profileId: ComplianceProfileId) { + return PROFILE_RULE_OVERRIDES[profileId][issue.id] ?? PROFILE_RULE_DEFAULT_SEVERITIES[issue.id] ?? issue.severity; +} + +function countIssuesForProfile(issues: readonly Issue[], profileId: ComplianceProfileId) { + return issues.reduce( + (summary, issue) => { + summary[getIssueSeverityForProfile(issue, profileId)] += 1; + return summary; + }, + { error: 0, warning: 0, info: 0 }, + ); +} + +const SCENARIO_PRESETS: readonly ScenarioPreset[] = [ + { + id: "inline-linear", + label: "Inline linear", + description: "Baseline inline linear ad with quartile tracking and playable media.", + groupId: "core", + versionLabel: "VAST 4.0", + focusAreas: ["inline", "linear", "quartiles"], + sourceMode: "xml", + action: "validate", + payload: sampleXml, + }, + { + id: "skippable-linear", + label: "Skippable linear", + description: "Linear playback with a real skip offset, skip beacon, and standard clickthrough handling.", + groupId: "core", + versionLabel: "VAST 4.1", + focusAreas: ["skippable", "linear", "playback"], + sourceMode: "xml", + action: "validate", + payload: skippableLinearXml, + }, + { + id: "broken-tag", + label: "Broken tag", + description: "Structural errors for missing required VAST 4.x elements.", + groupId: "core", + versionLabel: "VAST 4.2", + focusAreas: ["errors", "required fields"], + sourceMode: "xml", + action: "validate", + payload: brokenXml, + }, + { + id: "fixable-tag", + label: "Fixable tag", + description: "HTTPS upgrades that can be repaired deterministically.", + groupId: "core", + versionLabel: "VAST 4.1", + focusAreas: ["repair", "https"], + sourceMode: "xml", + action: "validate", + payload: fixableXml, + }, + { + id: "wrapper-chain", + label: "Wrapper chain", + description: "Local two-hop wrapper fixture for resolution demos.", + groupId: "core", + versionLabel: "VAST 4.x", + focusAreas: ["wrapper", "resolve", "waterfall"], + sourceMode: "url", + action: "resolve", + payload: "/scenarios/wrapper-root.xml", + }, + { + id: "pricing-category", + label: "Pricing and category", + description: "Commercial metadata coverage with pricing, content category, and progress beacons.", + groupId: "core", + versionLabel: "VAST 4.1", + focusAreas: ["pricing", "category", "metadata"], + sourceMode: "xml", + action: "validate", + payload: pricingCategoryXml, + }, + { + id: "non-linear-overlay", + label: "Non-linear overlay", + description: "Overlay creative coverage for non-linear placements and click-through handling.", + groupId: "creative-types", + versionLabel: "VAST 2.0", + focusAreas: ["non-linear", "overlay"], + sourceMode: "xml", + action: "validate", + payload: nonLinearOverlayXml, + }, + { + id: "companion-banner", + label: "Companion banner", + description: "Inline linear ad paired with a companion creative and companion click tracking.", + groupId: "creative-types", + versionLabel: "VAST 4.1", + focusAreas: ["companion", "banner"], + sourceMode: "xml", + action: "validate", + payload: companionBannerXml, + }, + { + id: "icon-fallbacks", + label: "Icon fallbacks", + description: "Ad icon payload with click fallback imagery for player-side disclosure handling.", + groupId: "creative-types", + versionLabel: "VAST 4.2", + focusAreas: ["icons", "fallbacks", "overlay"], + sourceMode: "xml", + action: "validate", + payload: iconFallbacksXml, + }, + { + id: "runtime-surfaces", + label: "Runtime surfaces", + description: "Companions, OMID verification resources, and VPAID markers.", + groupId: "creative-types", + versionLabel: "VAST 4.1", + focusAreas: ["verification", "icons", "companions"], + sourceMode: "xml", + action: "validate", + payload: runtimeSurfacesXml, + }, + { + id: "click-tracking", + label: "Click tracking", + description: "Video click-through, click tracking, and custom click URLs in one linear ad.", + groupId: "measurement", + versionLabel: "VAST 4.1", + focusAreas: ["clicks", "tracking"], + sourceMode: "xml", + action: "validate", + payload: clickTrackingXml, + }, + { + id: "viewable-impression", + label: "Viewable impression", + description: "Secondary impression reporting for viewable, not-viewable, and undetermined outcomes.", + groupId: "measurement", + versionLabel: "VAST 4.1", + focusAreas: ["viewability", "impression"], + sourceMode: "xml", + action: "validate", + payload: viewableImpressionXml, + }, + { + id: "ad-verification", + label: "Ad verification", + description: "OM SDK verification payloads with verification parameters and fallback tracking.", + groupId: "measurement", + versionLabel: "VAST 4.1", + focusAreas: ["omid", "verification"], + sourceMode: "xml", + action: "validate", + payload: adVerificationXml, + }, + { + id: "wrapper-signals", + label: "Wrapper signals", + description: "Wrapper-only measurement signals with inherited tracking, click beacons, and viewability hooks.", + groupId: "measurement", + versionLabel: "VAST 4.1", + focusAreas: ["wrapper", "beacons", "measurement"], + sourceMode: "xml", + action: "validate", + payload: wrapperSignalsXml, + }, + { + id: "ad-identity", + label: "Ad identity", + description: "Modern ad identity fields with AdServingId and UniversalAdId for partner QA.", + groupId: "ctv-ssai", + versionLabel: "VAST 4.1", + focusAreas: ["adservingid", "universaladid"], + sourceMode: "xml", + action: "validate", + payload: adIdentityXml, + }, + { + id: "mezzanine-support", + label: "Mezzanine support", + description: "CTV and SSAI-oriented tag carrying both ready-to-serve media and a mezzanine source.", + groupId: "ctv-ssai", + versionLabel: "VAST 4.1", + focusAreas: ["ssai", "mezzanine"], + sourceMode: "xml", + action: "validate", + payload: mezzanineSupportXml, + }, + { + id: "closed-captions", + label: "Closed captions", + description: "MediaFiles bundle with multiple caption resources for accessibility coverage.", + groupId: "ctv-ssai", + versionLabel: "VAST 4.2", + focusAreas: ["captions", "accessibility"], + sourceMode: "xml", + action: "validate", + payload: closedCaptionsXml, + }, +]; + +const SCENARIO_GROUPS: readonly ScenarioGroupDefinition[] = [ + { + id: "core", + label: "Core coverage", + description: "Baseline inline, skippable, metadata, wrapper resolution, and known-bad regression tags.", + }, + { + id: "creative-types", + label: "Creative formats", + description: "Non-linear, companion, icons, and mixed runtime surfaces that stress player integrations.", + }, + { + id: "measurement", + label: "Measurement", + description: "Click, wrapper, viewability, and OMID verification cases that tend to break analytics pipelines.", + }, + { + id: "ctv-ssai", + label: "CTV and SSAI", + description: "Identity, mezzanine, and captioning samples for modern distribution workflows.", + }, +]; + +const GROUPED_SCENARIO_PRESETS = SCENARIO_GROUPS.map((group) => ({ + ...group, + scenarios: SCENARIO_PRESETS.filter((scenario) => scenario.groupId === group.id), +})); + +const ACTION_LABELS: Record = { + validate: "Validate", + resolve: "Resolve wrappers", + fix: "Auto-fix", +}; + +function buildSource(sourceMode: SourceMode, payload: string): VastSessionSource { + if (sourceMode === "url") { + return { + kind: "url", + url: payload, + label: "Remote VAST tag", + }; + } + + return { + kind: "xml", + xml: payload, + label: "Editor XML", + }; +} + +function isValidRemoteUrl(value: string) { + try { + const parsed = new URL(value); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } +} + +function buildLocalAssetUrl(path: string) { + const origin = typeof globalThis.location === "object" ? globalThis.location.origin : DEFAULT_APP_ORIGIN; + const appBaseUrl = new URL(APP_BASE_PATH, origin.endsWith("/") ? origin : `${origin}/`); + const normalizedPath = path.startsWith("/") ? path.slice(1) : path; + return new URL(normalizedPath, appBaseUrl).toString(); +} + +function buildScenarioUrl(path: string) { + return buildLocalAssetUrl(path); +} + +function buildScenarioFixtureUrl(path: string) { + const normalizedPath = path.startsWith("/") ? path : `/${path}`; + if (typeof globalThis.location === "object" && globalThis.location.protocol === "https:") { + return buildLocalAssetUrl(normalizedPath); + } + + return new URL(normalizedPath, SCENARIO_FALLBACK_ASSET_ORIGIN).toString(); +} + +function absolutizeScenarioXmlLocalUrls(xml: string) { + return xml.replace(//g, (_match, path: string) => { + return ``; + }); +} + +function createTimestamp() { + return new Date().toISOString(); +} + +function createCacheBustingValue() { + return Math.floor(Math.random() * 100000000) + .toString() + .padStart(8, "0"); +} + +function createDraftId(prefix: string) { + return `${prefix}-${createTimestamp()}-${Math.random().toString(36).slice(2, 8)}`; +} + +function formatDimensions(width: string | null | undefined, height: string | null | undefined) { + const normalizedWidth = width && width.trim().length > 0 ? width : "?"; + const normalizedHeight = height && height.trim().length > 0 ? height : "?"; + + return normalizedWidth === "?" && normalizedHeight === "?" + ? "n/a" + : `${normalizedWidth} x ${normalizedHeight}`; +} + +function describeTransport(url: string | null, inlineLabel = "inline") { + if (!url) { + return inlineLabel; + } + + try { + const parsed = new URL(url); + if (parsed.protocol === "https:") { + return "HTTPS"; + } + + if (parsed.protocol === "http:") { + return "HTTP"; + } + + return parsed.protocol.replace(":", "").toUpperCase(); + } catch { + return inlineLabel; + } +} + +function elevateRisk(current: AssetRiskLevel, candidate: AssetRiskLevel) { + const order: Record = { + ok: 0, + attention: 1, + risk: 2, + }; + + return order[candidate] > order[current] ? candidate : current; +} + +function formatAssetRiskLabel(level: AssetRiskLevel) { + if (level === "ok") { + return "ready"; + } + + if (level === "attention") { + return "review"; + } + + return "high risk"; +} + +function buildEditorIssueMarkers(issues: readonly Issue[]) { + const issuesByLine = new Map(); + + for (const issue of issues) { + if (typeof issue.line !== "number" || !Number.isFinite(issue.line) || issue.line < 1) { + continue; + } + + const lineIssues = issuesByLine.get(issue.line) ?? []; + lineIssues.push(issue); + issuesByLine.set(issue.line, lineIssues); + } + + return [...issuesByLine.entries()] + .sort(([left], [right]) => left - right) + .map(([line, lineIssues]) => { + const severity = lineIssues.some((issue) => issue.severity === "error") + ? "error" + : lineIssues.some((issue) => issue.severity === "warning") + ? "warning" + : "info"; + const firstIssue = lineIssues[0]; + const summary = lineIssues.length > 1 + ? `${String(lineIssues.length)} findings on this line` + : firstIssue.message; + const title = lineIssues.map((issue) => `${issue.id}: ${issue.message}`).join("\n"); + + return { + id: `editor-line-${String(line)}`, + line, + severity, + issueCount: lineIssues.length, + summary, + title, + top: EDITOR_VERTICAL_PADDING + ((line - 1) * EDITOR_LINE_HEIGHT), + } satisfies EditorIssueMarker; + }); +} + +function encodeBase64Url(value: string) { + const bytes = new TextEncoder().encode(value); + let binary = ""; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + + return globalThis.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); +} + +function decodeBase64Url(value: string) { + const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); + const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - (normalized.length % 4)); + const binary = globalThis.atob(`${normalized}${padding}`); + const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); + return new TextDecoder().decode(bytes); +} + +function readSharedSessionState(): SharedSessionState | null { + if (typeof globalThis.location !== "object") { + return null; + } + + const params = new URLSearchParams(globalThis.location.hash.replace(/^#/, "")); + const encoded = params.get("session"); + if (!encoded) { + return null; + } + + try { + const parsed = JSON.parse(decodeBase64Url(encoded)) as Partial; + if (!parsed || (parsed.sourceMode !== "xml" && parsed.sourceMode !== "url")) { + return null; + } + + if (parsed.action !== "validate" && parsed.action !== "resolve" && parsed.action !== "fix") { + return null; + } + + if (typeof parsed.payload !== "string" || parsed.payload.length === 0) { + return null; + } + + return { + sourceMode: parsed.sourceMode, + action: parsed.action, + payload: parsed.payload, + activeScenarioId: typeof parsed.activeScenarioId === "string" ? parsed.activeScenarioId : null, + selectedComplianceProfileId: isComplianceProfileId(parsed.selectedComplianceProfileId) + ? parsed.selectedComplianceProfileId + : null, + }; + } catch { + return null; + } +} + +function formatRunSource(lastRun: RunRequest) { + return lastRun.sourceMode === "xml" ? `Editor XML (${String(lastRun.payload.length)} bytes)` : lastRun.payload; +} + +function formatClock(seconds: number | null) { + if (seconds === null || !Number.isFinite(seconds)) { + return "n/a"; + } + + const totalSeconds = Math.max(0, Math.floor(seconds)); + const minutes = Math.floor(totalSeconds / 60); + const remainingSeconds = totalSeconds % 60; + return `${String(minutes).padStart(2, "0")}:${String(remainingSeconds).padStart(2, "0")}`; +} + +function formatMacroPlayhead(seconds: number | null) { + if (seconds === null || !Number.isFinite(seconds)) { + return "00:00:00.000"; + } + + const totalMilliseconds = Math.max(0, Math.floor(seconds * 1000)); + const hours = Math.floor(totalMilliseconds / 3_600_000); + const minutes = Math.floor((totalMilliseconds % 3_600_000) / 60_000); + const wholeSeconds = Math.floor((totalMilliseconds % 60_000) / 1_000); + const milliseconds = totalMilliseconds % 1_000; + + return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(wholeSeconds).padStart(2, "0")}.${String(milliseconds).padStart(3, "0")}`; +} + +function summarizeWrapperValidation(validation: VastWrapperHop["validation"]) { + if (!validation) { + return "not validated"; + } + + return `${validation.summary.errors}e / ${validation.summary.warnings}w / ${validation.summary.infos}i`; +} + +function buildWrapperChangeNotes( + hop: VastWrapperHop, + previous: VastWrapperHop | null, + resolvedAd: VastResolvedAd | null, +) { + const notes: string[] = []; + + if (!previous) { + notes.push( + hop.source.kind === "url" + ? "Root document fetched from the requested URL." + : "Root document loaded from the XML editor.", + ); + } + + if (previous && previous.adType !== hop.adType) { + notes.push(`Ad type ${previous.adType} -> ${hop.adType}.`); + } + + if (previous && previous.adSystem !== hop.adSystem) { + notes.push(`Ad system ${previous.adSystem || "unknown"} -> ${hop.adSystem || "unknown"}.`); + } + + if (previous && previous.duration !== hop.duration) { + notes.push(`Duration ${previous.duration || "n/a"} -> ${hop.duration || "n/a"}.`); + } + + if (previous && previous.impressionCount !== hop.impressionCount) { + notes.push(`Impression URLs ${String(previous.impressionCount)} -> ${String(hop.impressionCount)}.`); + } + + if (previous && previous.trackingEventCount !== hop.trackingEventCount) { + notes.push(`Tracking events ${String(previous.trackingEventCount)} -> ${String(hop.trackingEventCount)}.`); + } + + if (previous && previous.companionCount !== hop.companionCount) { + notes.push(`Companions ${String(previous.companionCount)} -> ${String(hop.companionCount)}.`); + } + + if (hop.wrapperUri) { + notes.push(`Next wrapper URI: ${hop.wrapperUri}`); + } else if (hop.adType === "InLine") { + notes.push("Resolution terminates here with inline creative content."); + } + + if (resolvedAd) { + notes.push( + `Final payload from this hop includes ${String(resolvedAd.impressionUrls.length)} impression URL(s), ${String(resolvedAd.clickTrackingUrls.length)} click tracker(s), and ${String(resolvedAd.mediaFiles.length)} media file(s).`, + ); + } + + if (notes.length === 0) { + notes.push("No material metadata delta from the previous hop."); + } + + return notes.slice(0, 4); +} + +function buildWrapperInspectors( + wrapperChain: readonly VastWrapperHop[], + resolvedAds: readonly VastResolvedAd[], +) { + const resolvedByHop = new Map(); + + for (const resolvedAd of resolvedAds) { + if (resolvedAd.finalHopIndex !== null) { + resolvedByHop.set(resolvedAd.finalHopIndex, resolvedAd); + } + } + + return wrapperChain.map((hop, index) => ({ + id: `hop-${String(hop.index)}-${hop.url ?? hop.source.kind}`, + hopIndex: hop.index, + title: hop.adTitle || (hop.adType === "Wrapper" ? "Wrapper hop" : "Inline creative"), + adType: hop.adType, + adSystem: hop.adSystem || "unknown", + duration: hop.duration || "n/a", + sourceLabel: hop.url ?? (hop.source.kind === "url" ? hop.source.url : "Editor XML"), + nextHopLabel: hop.wrapperUri ?? (hop.adType === "InLine" ? "Resolved inline segment" : "No next hop"), + fetchedAt: hop.fetchedAt, + fetchMs: hop.fetchMs, + validationSummary: summarizeWrapperValidation(hop.validation), + tone: hop.validation + ? (hop.validation.summary.errors > 0 ? "error" : hop.validation.summary.warnings > 0 ? "warning" : "ok") + : "warning", + stats: [ + `${String(hop.fetchMs)} ms fetch`, + `${String(hop.impressionCount)} impression${hop.impressionCount === 1 ? "" : "s"}`, + `${String(hop.trackingEventCount)} tracker${hop.trackingEventCount === 1 ? "" : "s"}`, + `${String(hop.companionCount)} companion${hop.companionCount === 1 ? "" : "s"}`, + `${String(hop.mediaFiles.length)} media`, + ], + changes: buildWrapperChangeNotes(hop, wrapperChain[index - 1] ?? null, resolvedByHop.get(hop.index) ?? null), + } satisfies WrapperHopInspector)); +} + +function buildAssetAuditRows(resolvedAds: readonly VastResolvedAd[]) { + const rows: AssetAuditRow[] = []; + + for (const [adIndex, resolvedAd] of resolvedAds.entries()) { + const adTitle = resolvedAd.adTitle || resolvedAd.adPod.adId || `Ad ${String(adIndex + 1)}`; + + resolvedAd.mediaFiles.forEach((mediaFile, mediaIndex) => { + let riskLevel: AssetRiskLevel = "ok"; + const notes: string[] = []; + const transport = describeTransport(mediaFile.url); + + if (transport === "HTTP") { + riskLevel = elevateRisk(riskLevel, "risk"); + notes.push("Non-HTTPS media URL."); + } + + if (!mediaFile.width || !mediaFile.height) { + riskLevel = elevateRisk(riskLevel, "attention"); + notes.push("Missing declared dimensions."); + } + + if (!/^video\/(mp4|webm)$/i.test(mediaFile.mimeType) && mediaFile.mimeType !== "application/x-mpegURL") { + riskLevel = elevateRisk(riskLevel, "attention"); + notes.push("Uncommon playback MIME type."); + } + + if (mediaFile.bitrate) { + notes.push(`Bitrate ${mediaFile.bitrate}.`); + } + + rows.push({ + id: `media-${String(adIndex)}-${String(mediaIndex)}`, + assetType: "Media file", + adTitle, + format: [mediaFile.mimeType, mediaFile.delivery].filter(Boolean).join(" / "), + dimensions: formatDimensions(mediaFile.width, mediaFile.height), + transport, + riskLevel, + riskLabel: formatAssetRiskLabel(riskLevel), + detail: notes.join(" ") || "Primary playable media asset.", + url: mediaFile.url, + }); + }); + + resolvedAd.companions.forEach((companion, companionIndex) => { + const resources = companion.resources.length > 0 ? companion.resources : [null]; + + resources.forEach((resource, resourceIndex) => { + let riskLevel: AssetRiskLevel = "ok"; + const notes: string[] = []; + let format = "no creative resource"; + let transport = "inline"; + let url: string | null = companion.clickThroughUrl; + + if (resource) { + format = [resource.kind, resource.creativeType].filter(Boolean).join(" / "); + if (resource.kind === "html") { + transport = "INLINE HTML"; + riskLevel = elevateRisk(riskLevel, "attention"); + notes.push("Inline HTML companion resource."); + } else { + transport = describeTransport(resource.content, resource.kind.toUpperCase()); + url = resource.content; + if (transport === "HTTP") { + riskLevel = elevateRisk(riskLevel, "risk"); + notes.push("Non-HTTPS companion resource."); + } + } + + if (resource.kind === "iframe") { + riskLevel = elevateRisk(riskLevel, "attention"); + notes.push("Iframe rendering dependency."); + } + } else { + riskLevel = elevateRisk(riskLevel, "attention"); + notes.push("Companion has no declared creative resource."); + } + + if (!companion.clickThroughUrl) { + notes.push("No click-through URL."); + } else if (describeTransport(companion.clickThroughUrl) === "HTTP") { + riskLevel = elevateRisk(riskLevel, "risk"); + notes.push("HTTP click-through URL."); + } + + rows.push({ + id: `companion-${String(adIndex)}-${String(companionIndex)}-${String(resourceIndex)}`, + assetType: "Companion", + adTitle, + format, + dimensions: formatDimensions(companion.width, companion.height), + transport, + riskLevel, + riskLabel: formatAssetRiskLabel(riskLevel), + detail: notes.join(" ") || "Companion creative resource.", + url, + }); + }); + }); + + resolvedAd.icons.forEach((icon, iconIndex) => { + const resources = icon.resources.length > 0 ? icon.resources : [null]; + + resources.forEach((resource, resourceIndex) => { + let riskLevel: AssetRiskLevel = "ok"; + const notes: string[] = []; + let format = "no creative resource"; + let transport = "inline"; + let url: string | null = icon.clickThroughUrl; + + if (resource) { + format = [resource.kind, resource.creativeType].filter(Boolean).join(" / "); + if (resource.kind === "html") { + transport = "INLINE HTML"; + riskLevel = elevateRisk(riskLevel, "attention"); + notes.push("Inline HTML icon resource."); + } else { + transport = describeTransport(resource.content, resource.kind.toUpperCase()); + url = resource.content; + if (transport === "HTTP") { + riskLevel = elevateRisk(riskLevel, "risk"); + notes.push("Non-HTTPS icon resource."); + } + } + } else { + riskLevel = elevateRisk(riskLevel, "attention"); + notes.push("Icon has no declared creative resource."); + } + + if (!icon.viewTrackingUrls.length) { + notes.push("No icon view tracking URLs."); + } + + rows.push({ + id: `icon-${String(adIndex)}-${String(iconIndex)}-${String(resourceIndex)}`, + assetType: "Icon", + adTitle, + format, + dimensions: formatDimensions(icon.width, icon.height), + transport, + riskLevel, + riskLabel: formatAssetRiskLabel(riskLevel), + detail: notes.join(" ") || "Overlay icon resource.", + url, + }); + }); + }); + + resolvedAd.adVerifications.forEach((verification, verificationIndex) => { + verification.resources.forEach((resource, resourceIndex) => { + let riskLevel: AssetRiskLevel = "ok"; + const notes: string[] = []; + const transport = describeTransport(resource.url); + + if (transport === "HTTP") { + riskLevel = elevateRisk(riskLevel, "risk"); + notes.push("Non-HTTPS verification URL."); + } + + if (resource.kind === "executable") { + riskLevel = elevateRisk(riskLevel, "attention"); + notes.push("Executable verification dependency."); + } + + if (!resource.apiFramework) { + notes.push("No API framework declared."); + } + + rows.push({ + id: `verification-${String(adIndex)}-${String(verificationIndex)}-${String(resourceIndex)}`, + assetType: "Verification", + adTitle, + format: [resource.kind, resource.mimeType].filter(Boolean).join(" / "), + dimensions: "n/a", + transport, + riskLevel, + riskLabel: formatAssetRiskLabel(riskLevel), + detail: [verification.vendor ?? "unknown vendor", resource.apiFramework, notes.join(" ")] + .filter(Boolean) + .join(" · "), + url: resource.url, + }); + }); + }); + } + + return rows; +} + +function buildComplianceVerdict( + id: ComplianceProfileId, + label: string, + description: string, + failures: string[], + cautions: string[], +): ComplianceProfileVerdict { + const status: ComplianceProfileStatus = failures.length > 0 ? "fail" : cautions.length > 0 ? "attention" : "pass"; + const summary = status === "pass" + ? "No blocking conditions found for this lens." + : status === "attention" + ? "Usable, but QA should review the highlighted edge cases." + : "Likely blocked under this compatibility lens."; + + return { + id, + label, + description, + status, + summary, + reasons: failures.length > 0 + ? [...failures, ...cautions].slice(0, 4) + : cautions.length > 0 + ? cautions.slice(0, 4) + : ["No blocking conditions found for this lens."], + }; +} + +function buildComplianceVerdicts( + validationReady: boolean, + issues: readonly Issue[], + resolvedAds: readonly VastResolvedAd[], + runtimeInspection: RuntimeInspection, + wrapperChain: readonly VastWrapperHop[], + assetAuditRows: readonly AssetAuditRow[], +) { + if (!validationReady && resolvedAds.length === 0 && wrapperChain.length === 0) { + return [ + { + id: "strict-iab", + label: "Strict IAB", + description: "Raw spec posture for partner escalation.", + status: "attention", + summary: "Run validate, resolve, or prepare the runner to score this profile.", + reasons: ["No validation or resolved inventory has been collected yet."], + }, + { + id: "ctv-safe", + label: "CTV-safe", + description: "TV playback with conservative runtime assumptions.", + status: "attention", + summary: "Run validate, resolve, or prepare the runner to score this profile.", + reasons: ["No resolved media inventory is available yet."], + }, + { + id: "ssai-safe", + label: "SSAI-safe", + description: "Server-side stitching with transport and beacon discipline.", + status: "attention", + summary: "Run validate, resolve, or prepare the runner to score this profile.", + reasons: ["No resolved tracking inventory is available yet."], + }, + { + id: "legacy-player", + label: "Legacy player", + description: "Older player stacks that prefer simpler media and overlays.", + status: "attention", + summary: "Run validate, resolve, or prepare the runner to score this profile.", + reasons: ["No resolved asset inventory is available yet."], + }, + ] satisfies ComplianceProfileVerdict[]; + } + + const strictCounts = countIssuesForProfile(issues, "strict-iab"); + const ctvCounts = countIssuesForProfile(issues, "ctv-safe"); + const ssaiCounts = countIssuesForProfile(issues, "ssai-safe"); + const legacyCounts = countIssuesForProfile(issues, "legacy-player"); + const mediaFiles = resolvedAds.flatMap((resolvedAd) => resolvedAd.mediaFiles); + const hasMp4 = mediaFiles.some((mediaFile) => mediaFile.mimeType === "video/mp4"); + const hasHls = mediaFiles.some((mediaFile) => mediaFile.mimeType === "application/x-mpegURL"); + const hasSimpleVideo = mediaFiles.some((mediaFile) => /^video\/(mp4|webm)$/i.test(mediaFile.mimeType)); + const hasErrorTracking = resolvedAds.some((resolvedAd) => resolvedAd.errorUrls.length > 0); + const hasImpressionTracking = resolvedAds.some((resolvedAd) => resolvedAd.impressionUrls.length > 0); + const httpAssets = assetAuditRows.filter((row) => row.transport === "HTTP").length; + const highRiskAssets = assetAuditRows.filter((row) => row.riskLevel === "risk").length; + const reviewAssets = assetAuditRows.filter((row) => row.riskLevel === "attention").length; + const verificationCount = runtimeInspection.verificationResources.length; + const vpaidCount = runtimeInspection.vpaidCount; + const iconCount = runtimeInspection.icons.length; + + return [ + buildComplianceVerdict( + "strict-iab", + "Strict IAB", + "Raw spec posture for partner escalation.", + strictCounts.error > 0 ? [`${String(strictCounts.error)} error-severity rule violation(s) remain.`] : [], + [ + ...(strictCounts.warning > 0 ? [`${String(strictCounts.warning)} warning(s) still need review.`] : []), + ...(wrapperChain.some((hop) => (hop.validation?.summary.warnings ?? 0) > 0) + ? ["One or more wrapper hops carry warning-level findings."] + : []), + ], + ), + buildComplianceVerdict( + "ctv-safe", + "CTV-safe", + "TV playback with conservative runtime assumptions.", + [ + ...(ctvCounts.error > 0 ? [`${String(ctvCounts.error)} rule violation(s) fail this validation mode.`] : []), + ...(mediaFiles.length === 0 ? ["No playable media assets were resolved."] : []), + ...(!hasMp4 && !hasHls ? ["No MP4 or HLS media is available for TV-class playback."] : []), + ...(vpaidCount > 0 ? [`${String(vpaidCount)} VPAID marker(s) are present and likely unsupported on CTV.`] : []), + ], + [ + ...(verificationCount > 0 ? [`${String(verificationCount)} verification resource(s) may require device-specific support.`] : []), + ...(highRiskAssets > 0 ? [`${String(highRiskAssets)} asset(s) still rely on insecure or execution-heavy delivery.`] : []), + ...(ctvCounts.warning > 0 ? [`${String(ctvCounts.warning)} additional warning(s) still need QA sign-off.`] : []), + ], + ), + buildComplianceVerdict( + "ssai-safe", + "SSAI-safe", + "Server-side stitching with transport and beacon discipline.", + [ + ...(ssaiCounts.error > 0 ? [`${String(ssaiCounts.error)} rule violation(s) fail this validation mode.`] : []), + ...(!hasImpressionTracking ? ["No impression tracking URLs are present."] : []), + ...(!hasErrorTracking ? ["No error tracking URLs are present."] : []), + ...(httpAssets > 0 ? [`${String(httpAssets)} asset URL(s) still use HTTP transport.`] : []), + ], + [ + ...(verificationCount > 0 || vpaidCount > 0 + ? ["Client-side verification or VPAID dependencies reduce SSAI portability."] + : []), + ...(wrapperChain.length > 2 ? [`Wrapper chain depth is ${String(wrapperChain.length)} hops.`] : []), + ...(ssaiCounts.warning > 0 ? [`${String(ssaiCounts.warning)} additional warning(s) still need QA sign-off.`] : []), + ...(reviewAssets > 0 ? [`${String(reviewAssets)} asset(s) merit manual review.`] : []), + ], + ), + buildComplianceVerdict( + "legacy-player", + "Legacy player", + "Older player stacks that prefer simpler media and overlays.", + [ + ...(legacyCounts.error > 0 ? [`${String(legacyCounts.error)} rule violation(s) still block this compatibility mode.`] : []), + ...(!hasSimpleVideo ? ["No simple MP4 or WebM file is available for older players."] : []), + ], + [ + ...(legacyCounts.warning > 0 ? [`${String(legacyCounts.warning)} warning(s) remain after compatibility downgrades.`] : []), + ...(verificationCount > 0 ? [`${String(verificationCount)} verification resource(s) may exceed older player support.`] : []), + ...(iconCount > 0 ? [`${String(iconCount)} icon resource(s) require additional player surface support.`] : []), + ...(vpaidCount > 0 ? [`${String(vpaidCount)} VPAID marker(s) need explicit player testing.`] : []), + ...(highRiskAssets > 0 ? [`${String(highRiskAssets)} asset(s) still need transport cleanup.`] : []), + ], + ), + ]; +} + +function buildMacroEntryDrafts(macros: Record) { + return Object.entries(macros).map(([key, value], index) => ({ + id: `${key}-${String(index)}`, + key, + value, + } satisfies MacroEntryDraft)); +} + +function buildMacroRecord(entries: readonly MacroEntryDraft[]) { + return entries.reduce>((current, entry) => { + const normalizedKey = entry.key.trim().toUpperCase(); + if (!normalizedKey) { + return current; + } + + current[normalizedKey] = entry.value; + return current; + }, {}); +} + +function expandTrackingPreviewUrl( + url: string, + macros: Record, + defaults: Record, +) { + const values: Record = { + ...defaults, + }; + + for (const [key, value] of Object.entries(macros)) { + values[key.toUpperCase()] = String(value); + } + + return url.replace(/\[([A-Z0-9_]+)\]|%%([A-Z0-9_]+)%%/gi, (match, bracketName, legacyName) => { + const macroKey = String(bracketName ?? legacyName ?? "").toUpperCase(); + const replacement = values[macroKey]; + return replacement === undefined ? match : encodeURIComponent(replacement); + }); +} + +function buildMacroPresetDefinitions( + defaults: Record, + currentTimeSec: number | null, + muted: boolean, + viewability: VastPlaybackViewability | null, + resolvedAd: VastResolvedAd | null, +) { + const playhead = formatMacroPlayhead(currentTimeSec); + const userAgent = typeof globalThis.navigator === "object" + ? globalThis.navigator.userAgent + : "Mozilla/5.0 (compatible; VastValidator/1.0)"; + const pageUrl = typeof globalThis.location === "object" + ? globalThis.location.href + : "https://publisher.example.com/player"; + const adSequence = String(resolvedAd?.adPod.sequence ?? 1); + const adServingId = resolvedAd?.adPod.adId ?? resolvedAd?.adTitle ?? "demo-ad"; + const playerState = `muted=${muted ? "1" : "0"};viewability=${viewability ?? "unset"}`; + + return [ + { + id: "web-browser", + label: "Web browser", + description: "Desktop browser autoplay-muted environment.", + macros: { + ...defaults, + ERRORCODE: "901", + CONTENTPLAYHEAD: playhead, + PAGEURL: pageUrl, + DEVICEUA: userAgent, + PLAYERSTATE: playerState, + PODSEQUENCE: adSequence, + }, + }, + { + id: "mobile-app", + label: "Mobile app", + description: "In-app SDK context with app bundle identifiers.", + macros: { + ...defaults, + ERRORCODE: "402", + CONTENTPLAYHEAD: playhead, + PAGEURL: "app://publisher/feed/featured", + APPBUNDLE: "com.publisher.mobile", + DEVICEUA: "PublisherMobileSDK/7.2 (iPhone; iOS 18.0)", + PLAYERSTATE: playerState, + }, + }, + { + id: "ctv-player", + label: "CTV player", + description: "Connected TV runtime with large-screen user agent values.", + macros: { + ...defaults, + ERRORCODE: "901", + CONTENTPLAYHEAD: playhead, + APPBUNDLE: "com.publisher.ctv", + DEVICEUA: "Roku/DVP-12.5 (519.10E04154A)", + PAGEURL: "https://publisher.example.com/ctv/home", + PODSEQUENCE: adSequence, + PLAYERSTATE: playerState, + }, + }, + { + id: "ssai-proxy", + label: "SSAI proxy", + description: "Server-side ad insertion / stitcher macro set.", + macros: { + ...defaults, + ERRORCODE: "303", + CONTENTPLAYHEAD: playhead, + PAGEURL: "https://ssai.publisher.example.com/live/master.m3u8", + SERVERUA: "Akamai-SSAI/1.0", + ADSERVINGID: adServingId, + PODSEQUENCE: adSequence, + PLAYERSTATE: playerState, + }, + }, + ] satisfies MacroPresetDefinition[]; +} + +function buildTrackingHistoryKey(event: string, url: string, hopIndex: number, offset: string | null) { + return `${event}:${String(hopIndex)}:${offset ?? ""}:${url}`; +} + +function buildTrackingWaterfall( + plan: VastTrackingPlan, + history: readonly VastTrackingDispatchResult[], + macros: Record, + defaults: Record, +) { + const historyByKey = new Map(); + + for (const entry of history) { + const key = buildTrackingHistoryKey(entry.event, entry.url, entry.hopIndex, entry.offset); + const current = historyByKey.get(key) ?? []; + current.push(entry); + historyByKey.set(key, current); + } + + const seededTargets: Array<{ event: string; target: VastTrackingTarget }> = [ + ...plan.impressions.map((target) => ({ event: "impression", target })), + ...plan.errors.map((target) => ({ event: "error", target })), + ...plan.clickTrackings.map((target) => ({ event: "clickTracking", target })), + ...plan.clickThroughs.map((target) => ({ event: "clickThrough", target })), + ...plan.events.map((target) => ({ event: target.event, target })), + ]; + + const eventOrder = new Map([ + ["impression", 0], + ["creativeView", 1], + ["start", 2], + ["firstQuartile", 3], + ["midpoint", 4], + ["thirdQuartile", 5], + ["complete", 6], + ["pause", 7], + ["resume", 8], + ["mute", 9], + ["unmute", 10], + ["fullscreen", 11], + ["exitFullscreen", 12], + ["viewable", 13], + ["notViewable", 14], + ["viewUndetermined", 15], + ["clickTracking", 16], + ["clickThrough", 17], + ["skip", 18], + ["error", 19], + ]); + + return seededTargets + .map(({ event, target }, index) => { + const key = buildTrackingHistoryKey(event, target.url, target.hopIndex, target.offset); + const results = event === "clickThrough" ? [] : (historyByKey.get(key) ?? []); + const latest = results.at(-1) ?? null; + const status: TrackingWaterfallStatus = event === "clickThrough" + ? "linked" + : latest + ? (latest.ok ? "ok" : "failed") + : "ready"; + + return { + id: `${event}-${String(index)}-${target.url}`, + event, + kind: target.kind, + status, + originalUrl: target.url, + expandedUrl: latest?.resolvedUrl ?? expandTrackingPreviewUrl(target.url, macros, defaults), + hopIndex: target.hopIndex, + sourceUrl: target.sourceUrl, + offset: target.offset, + dispatchCount: results.length, + lastDispatchedAt: latest?.dispatchedAt ?? null, + httpStatus: latest?.status ?? null, + error: latest?.error ?? null, + } satisfies TrackingWaterfallRow; + }) + .sort((left, right) => { + const leftOrder = eventOrder.get(left.event) ?? 99; + const rightOrder = eventOrder.get(right.event) ?? 99; + if (left.hopIndex !== right.hopIndex) { + return left.hopIndex - right.hopIndex; + } + + if (leftOrder !== rightOrder) { + return leftOrder - rightOrder; + } + + return left.originalUrl.localeCompare(right.originalUrl); + }); +} + +function buildArtifactReadme( + scenarioLabel: string | null, + macroPreset: MacroPresetDefinition | null, + complianceProfile: ComplianceProfileVerdict | null, + waterfallCount: number, + timelineCount: number, + assetCount: number, +) { + return [ + "VAST Validator Artifact Bundle", + `Scenario: ${scenarioLabel ?? "Custom input"}`, + `Macro preset: ${macroPreset?.label ?? "Custom"}`, + `Compliance lens: ${complianceProfile ? `${complianceProfile.label} (${complianceProfile.status})` : "n/a"}`, + `Tracking rows: ${String(waterfallCount)}`, + `Timeline entries: ${String(timelineCount)}`, + `Asset audit rows: ${String(assetCount)}`, + "", + "Included artifacts:", + "- report.txt / report.json: current validation summary", + "- source/: original XML or URL references and optional fixed XML", + "- runtime/: playback snapshot, timeline, macro set, and tracking waterfall", + "- validation/: wrapper chain, compliance verdicts, asset audit, and resolved-ad metadata", + ].join("\n"); +} + +function buildTrackingFetch(documentUrls: readonly string[]): typeof fetch { + const documentUrlSet = new Set(documentUrls); + + return async (input, init) => { + const inputUrl = typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; + const normalizedUrl = new URL(inputUrl, typeof globalThis.location === "object" ? globalThis.location.origin : "http://localhost").toString(); + const looksLikeDocument = documentUrlSet.has(normalizedUrl) || /\.xml(?:$|[?#])/i.test(normalizedUrl); + + if (looksLikeDocument) { + return globalThis.fetch(input, init); + } + + return new Response(null, { + status: 204, + statusText: "No Content", + }); + }; +} + +function buildPlayableMediaUrl(mediaUrl: string | null) { + if (!mediaUrl) { + return null; + } + + const origin = typeof globalThis.location === "object" ? globalThis.location.origin : "http://localhost:5175"; + + try { + const parsed = new URL(mediaUrl); + const pathname = parsed.pathname.toLowerCase(); + if (parsed.hostname === "example.com" && pathname.endsWith("/fixtures/video/360p30.webm")) { + return buildLocalAssetUrl("fixtures/video/360p30.webm"); + } + + if (parsed.hostname === "example.com" && pathname.endsWith("/fixtures/video/360p30.mp4")) { + return buildLocalAssetUrl("fixtures/video/360p30.mp4"); + } + } catch { + return mediaUrl; + } + + return mediaUrl; +} + +function collectApiFrameworks(xml: string | null): string[] { + if (!xml) { + return []; + } + + const frameworks = [...xml.matchAll(/apiFramework=(?:"([^"]+)"|'([^']+)')/gi)] + .map((match) => (match[1] ?? match[2] ?? "").trim()) + .filter((value) => value.length > 0); + + return [...new Set(frameworks)]; +} + +function buildRuntimeInspection(rootXml: string | null, resolvedAds: readonly VastResolvedAd[]): RuntimeInspection { + const apiFrameworks = collectApiFrameworks(rootXml); + const verificationResources = resolvedAds.flatMap((resolvedAd, adIndex) => + resolvedAd.adVerifications.flatMap((verification, verificationIndex) => + verification.resources.map((resource, resourceIndex) => ({ + id: `${String(adIndex)}-${String(verificationIndex)}-${String(resourceIndex)}`, + adTitle: resolvedAd.adTitle || resolvedAd.adPod.adId || `Ad ${String(adIndex + 1)}`, + vendor: verification.vendor ?? "unknown", + kind: resource.kind, + apiFramework: resource.apiFramework, + url: resource.url, + })), + ), + ); + + const companions = resolvedAds.flatMap((resolvedAd, adIndex) => + resolvedAd.companions.map((companion, companionIndex) => ({ + id: `companion-${String(adIndex)}-${String(companionIndex)}`, + adTitle: resolvedAd.adTitle || resolvedAd.adPod.adId || `Ad ${String(adIndex + 1)}`, + title: `${companion.width} x ${companion.height}`, + resource: companion.resources[0] ?? null, + clickThroughUrl: companion.clickThroughUrl, + })), + ); + + const icons = resolvedAds.flatMap((resolvedAd, adIndex) => + resolvedAd.icons.map((icon, iconIndex) => ({ + id: `icon-${String(adIndex)}-${String(iconIndex)}`, + adTitle: resolvedAd.adTitle || resolvedAd.adPod.adId || `Ad ${String(adIndex + 1)}`, + title: `${icon.width} x ${icon.height}`, + resource: icon.resources[0] ?? null, + clickThroughUrl: icon.clickThroughUrl, + })), + ); + + return { + apiFrameworks, + verificationResources, + companions, + icons, + omidCount: verificationResources.filter((resource) => resource.apiFramework?.toLowerCase() === "omid").length, + vpaidCount: apiFrameworks.filter((framework) => framework.toUpperCase() === "VPAID").length, + }; +} + +function buildReportSummary( + lastRun: RunRequest, + snapshot: VastSessionSnapshot, + severity: ReturnType, + issues: readonly Issue[], + resolvedAds: readonly VastResolvedAd[], + scenarioLabel: string | null, + lastFix: FixResult | null, + activeComplianceVerdict: ComplianceProfileVerdict | null, +) { + const lines = [ + "VAST Validator Report", + `Scenario: ${scenarioLabel ?? "Custom input"}`, + `Source: ${formatRunSource(lastRun)}`, + `Action: ${lastRun.action}`, + `Status: ${snapshot.status}`, + `Version: ${snapshot.validation?.version ?? "unknown"}`, + `Validity: ${snapshot.validation?.summary.valid ? "valid" : "not valid"}`, + `Compliance lens: ${activeComplianceVerdict ? `${activeComplianceVerdict.label} (${activeComplianceVerdict.status})` : "n/a"}`, + `Counts: ${String(severity.error)} error(s), ${String(severity.warning)} warning(s), ${String(severity.info)} info`, + `Wrapper hops: ${String(snapshot.wrapperChain.length)}`, + `Resolved ads: ${String(resolvedAds.length)}`, + ]; + + if (activeComplianceVerdict) { + lines.push(`Profile verdict: ${activeComplianceVerdict.summary}`); + } + + if (lastFix) { + lines.push(`Fixes applied: ${String(lastFix.applied.length)} deterministic repair(s)`); + lines.push(`Remaining post-fix issues: ${String(lastFix.remaining.length)}`); + } + + if (issues.length > 0) { + lines.push("Top findings:"); + for (const issue of issues.slice(0, 5)) { + lines.push(`- [${issue.severity}] ${issue.id} at ${formatIssueLocation(issue)}: ${issue.message}`); + } + } else { + lines.push("Top findings: none"); + } + + if (resolvedAds.length > 0) { + lines.push("Resolved inventory:"); + for (const resolvedAd of resolvedAds.slice(0, 3)) { + lines.push( + `- ${resolvedAd.adTitle || resolvedAd.adPod.adId || "Untitled ad"} | ${resolvedAd.adType} | ${String(resolvedAd.mediaFiles.length)} media file(s) | ${resolvedAd.duration || "n/a"}`, + ); + } + } + + return lines.join("\n"); +} + +function buildErrorClipboardText( + lastRun: RunRequest, + scenarioLabel: string | null, + activeComplianceVerdict: ComplianceProfileVerdict | null, + issues: readonly Issue[], +) { + const errorIssues = issues.filter((issue) => issue.severity === "error"); + const warningCount = issues.filter((issue) => issue.severity === "warning").length; + const lines = [ + "VAST Validator Error Export", + `Scenario: ${scenarioLabel ?? "Custom input"}`, + `Source: ${formatRunSource(lastRun)}`, + `Action: ${lastRun.action}`, + `Compliance lens: ${activeComplianceVerdict ? `${activeComplianceVerdict.label} (${activeComplianceVerdict.status})` : "n/a"}`, + `Exported: ${new Date().toISOString()}`, + "", + ]; + + if (errorIssues.length === 0) { + lines.push("No error-severity findings are present in the current run."); + } else { + lines.push(`Errors: ${String(errorIssues.length)}`); + lines.push(""); + for (const [index, issue] of errorIssues.entries()) { + lines.push(`${String(index + 1)}. ${issue.id}`); + lines.push(` Location: ${formatIssueLocation(issue)}`); + lines.push(` Message: ${issue.message}`); + lines.push(` Spec: ${issue.spec_ref}`); + } + } + + if (warningCount > 0) { + lines.push(""); + lines.push(`Warnings not included in this export: ${String(warningCount)}`); + } + + return lines.join("\n"); +} + +function countBySeverity(issues: readonly Issue[]) { + return issues.reduce( + (summary, issue) => { + summary[issue.severity] += 1; + return summary; + }, + { error: 0, warning: 0, info: 0 }, + ); +} + +function formatIssueLocation(issue: Issue) { + if (issue.line === null) { + return issue.path ?? "document"; + } + + return `L${issue.line}${issue.col ? `:${issue.col}` : ""}`; +} + +function buildOverviewTone(valid: boolean | null, issueCount: number, resolvedCount: number) { + if (valid === true && resolvedCount > 0) { + return "valid-resolved"; + } + + if (valid === false || issueCount > 0) { + return "attention"; + } + + return "idle"; +} + +function App() { + const sharedSession = useMemo(() => readSharedSessionState(), []); + const [sourceMode, setSourceMode] = useState(sharedSession?.sourceMode ?? "xml"); + const [xmlDraft, setXmlDraft] = useState(sharedSession?.sourceMode === "xml" ? sharedSession.payload : sampleXml); + const [urlDraft, setUrlDraft] = useState(""); + const [activeScenarioId, setActiveScenarioId] = useState(sharedSession?.activeScenarioId ?? null); + const [scenarioVersionFilter, setScenarioVersionFilter] = useState("all"); + const [scenarioActionFilter, setScenarioActionFilter] = useState("all"); + const [scenarioSurfaceFilter, setScenarioSurfaceFilter] = useState("all"); + const [lastRun, setLastRun] = useState({ + id: 1, + sourceMode: sharedSession?.sourceMode ?? "xml", + action: sharedSession?.action ?? "validate", + payload: sharedSession?.payload ?? sampleXml, + }); + const [lastFix, setLastFix] = useState(null); + const [runError, setRunError] = useState(null); + const [reportNotice, setReportNotice] = useState(null); + const [selectedComplianceProfileId, setSelectedComplianceProfileId] = useState(sharedSession?.selectedComplianceProfileId ?? "strict-iab"); + const [selectedFindingLine, setSelectedFindingLine] = useState(null); + const [runnerTimeline, setRunnerTimeline] = useState([]); + const [editorScrollTop, setEditorScrollTop] = useState(0); + const findingsSectionRef = useRef(null); + const runnerEventCounter = useRef(0); + const runnerVideoRef = useRef(null); + const xmlTextareaRef = useRef(null); + const runnerProgressBucketRef = useRef(-1); + const macroDefaultsRef = useRef({ + CACHEBUSTING: createCacheBustingValue(), + TIMESTAMP: createTimestamp(), + }); + const macroEntryCounterRef = useRef(0); + + useEffect(() => { + if (sharedSession?.sourceMode === "url") { + setUrlDraft(sharedSession.payload); + } + }, [sharedSession]); + + const sessionSource = useMemo( + () => buildSource(lastRun.sourceMode, lastRun.payload), + [lastRun], + ); + const activeValidateOptions = useMemo( + () => buildComplianceValidateOptions(selectedComplianceProfileId), + [selectedComplianceProfileId], + ); + + const { snapshot, session } = useVastSession({ + source: sessionSource, + autoLoad: false, + autoValidate: false, + validateOptions: activeValidateOptions, + }); + + useEffect(() => { + let cancelled = false; + + async function runCurrentAction() { + setRunError(null); + + try { + if (lastRun.action === "fix") { + const result = await session.fix(); + if (!cancelled) { + setLastFix(result); + } + return; + } + + if (lastRun.action === "resolve") { + await session.resolve(); + if (!cancelled) { + setLastFix(null); + } + return; + } + + await session.validate(); + if (!cancelled) { + setLastFix(null); + } + } catch (error) { + if (!cancelled) { + setRunError(error instanceof Error ? error.message : String(error)); + } + } + } + + void runCurrentAction(); + + return () => { + cancelled = true; + }; + }, [lastRun.id, lastRun.action, session]); + + const issues = snapshot.validation?.issues ?? []; + const severity = countBySeverity(issues); + const resolvedAds = snapshot.resolvedAds; + const activeScenario = SCENARIO_PRESETS.find((scenario) => scenario.id === activeScenarioId) ?? null; + const scenarioVersionOptions = useMemo( + () => Array.from(new Set(SCENARIO_PRESETS.map((scenario) => scenario.versionLabel))), + [], + ); + const scenarioActionOptions = useMemo( + () => Array.from(new Set(SCENARIO_PRESETS.map((scenario) => scenario.action))), + [], + ); + const scenarioSurfaceOptions = useMemo( + () => Array.from(new Set(SCENARIO_PRESETS.flatMap((scenario) => scenario.focusAreas))).sort((left, right) => left.localeCompare(right)), + [], + ); + const filteredScenarioGroups = useMemo( + () => GROUPED_SCENARIO_PRESETS.map((group) => ({ + ...group, + scenarios: group.scenarios.filter((scenario) => { + if (scenarioVersionFilter !== "all" && scenario.versionLabel !== scenarioVersionFilter) { + return false; + } + + if (scenarioActionFilter !== "all" && scenario.action !== scenarioActionFilter) { + return false; + } + + if (scenarioSurfaceFilter !== "all" && !scenario.focusAreas.includes(scenarioSurfaceFilter)) { + return false; + } + + return true; + }), + })).filter((group) => group.scenarios.length > 0), + [scenarioActionFilter, scenarioSurfaceFilter, scenarioVersionFilter], + ); + const filteredScenarioCount = useMemo( + () => filteredScenarioGroups.reduce((count, group) => count + group.scenarios.length, 0), + [filteredScenarioGroups], + ); + const hasScenarioFilters = scenarioVersionFilter !== "all" || scenarioActionFilter !== "all" || scenarioSurfaceFilter !== "all"; + const activeScenarioMatchesFilters = activeScenario === null + ? true + : filteredScenarioGroups.some((group) => group.scenarios.some((scenario) => scenario.id === activeScenario.id)); + const overviewTone = buildOverviewTone(snapshot.validation?.summary.valid ?? null, issues.length, resolvedAds.length); + const activePayload = sourceMode === "xml" ? xmlDraft : urlDraft; + const trimmedPayload = activePayload.trim(); + const hasValidUrlInput = sourceMode === "url" ? isValidRemoteUrl(trimmedPayload) : true; + const canRun = sourceMode === "xml" ? trimmedPayload.length > 0 : hasValidUrlInput; + const editorAnnotationsMatchPayload = sourceMode === "xml" && lastRun.sourceMode === "xml" && lastRun.payload === xmlDraft; + const editorIssueMarkers = useMemo( + () => editorAnnotationsMatchPayload ? buildEditorIssueMarkers(issues) : [], + [editorAnnotationsMatchPayload, issues], + ); + const editorDocumentIssueCount = useMemo( + () => editorAnnotationsMatchPayload + ? issues.filter((issue) => typeof issue.line !== "number" || !Number.isFinite(issue.line) || issue.line < 1).length + : 0, + [editorAnnotationsMatchPayload, issues], + ); + const editorAnnotationsStale = sourceMode === "xml" && lastRun.sourceMode === "xml" && lastRun.payload !== xmlDraft && issues.length > 0; + const displayedIssues = useMemo( + () => selectedFindingLine === null ? issues : issues.filter((issue) => issue.line === selectedFindingLine), + [issues, selectedFindingLine], + ); + const runnerDocumentUrls = useMemo(() => { + const urls = new Set(); + if (lastRun.sourceMode === "url") { + urls.add(lastRun.payload); + } + + for (const hop of snapshot.wrapperChain) { + if (hop.url) { + urls.add(hop.url); + } + } + + if (snapshot.resolvedAd?.finalUrl) { + urls.add(snapshot.resolvedAd.finalUrl); + } + + return [...urls]; + }, [lastRun.payload, lastRun.sourceMode, snapshot.resolvedAd?.finalUrl, snapshot.wrapperChain]); + const runnerFetch = useMemo( + () => buildTrackingFetch(runnerDocumentUrls), + [runnerDocumentUrls], + ); + const runnerSession = useMemo( + () => createVastSession({ + source: buildSource(lastRun.sourceMode, lastRun.payload), + fetch: runnerFetch, + maxWrapperDepth: 5, + validateOptions: activeValidateOptions, + }), + [activeValidateOptions, lastRun.payload, lastRun.sourceMode, runnerFetch], + ); + const playback = useVastPlayback({ + session: runnerSession, + autoInitialize: false, + mediaSelection: { + supportedMimeTypes: ["video/mp4", "video/webm", "application/x-mpegURL"], + preferredMimeTypes: ["video/mp4", "video/webm"], + }, + }); + const runnerTracker = useVastTracker({ session: runnerSession }); + const runnerSnapshot = playback.snapshot; + const inventoryAds = useMemo( + () => (resolvedAds.length > 0 ? resolvedAds : runnerSnapshot.resolvedAd ? [runnerSnapshot.resolvedAd] : []), + [resolvedAds, runnerSnapshot.resolvedAd], + ); + const inspectionXml = snapshot.rootXml ?? (lastRun.sourceMode === "xml" ? lastRun.payload : null); + const runtimeInspection = useMemo( + () => buildRuntimeInspection(inspectionXml, inventoryAds), + [inspectionXml, inventoryAds], + ); + const runnerMediaUrl = useMemo( + () => buildPlayableMediaUrl(runnerSnapshot.mediaSelection.selected?.url ?? null), + [runnerSnapshot.mediaSelection.selected?.url], + ); + const macroPresets = useMemo( + () => buildMacroPresetDefinitions( + macroDefaultsRef.current, + runnerSnapshot.currentTimeSec, + runnerSnapshot.muted, + runnerSnapshot.viewability, + runnerSnapshot.resolvedAd, + ), + [runnerSnapshot.currentTimeSec, runnerSnapshot.muted, runnerSnapshot.resolvedAd, runnerSnapshot.viewability], + ); + const [selectedMacroPresetId, setSelectedMacroPresetId] = useState("web-browser"); + const [macroEntries, setMacroEntries] = useState(() => buildMacroEntryDrafts({ + CACHEBUSTING: createCacheBustingValue(), + TIMESTAMP: createTimestamp(), + ERRORCODE: "901", + CONTENTPLAYHEAD: "00:00:00.000", + PAGEURL: "https://publisher.example.com/player", + })); + const activeMacroPreset = macroPresets.find((preset) => preset.id === selectedMacroPresetId) ?? macroPresets[0] ?? null; + const activeMacros = useMemo(() => buildMacroRecord(macroEntries), [macroEntries]); + const trackingWaterfallRows = useMemo( + () => buildTrackingWaterfall(runnerTracker.tracking.plan, runnerTracker.tracking.history, activeMacros, macroDefaultsRef.current), + [activeMacros, runnerTracker.tracking.history, runnerTracker.tracking.plan], + ); + const waterfallSummary = useMemo(() => ({ + total: trackingWaterfallRows.length, + ok: trackingWaterfallRows.filter((row) => row.status === "ok").length, + failed: trackingWaterfallRows.filter((row) => row.status === "failed").length, + pending: trackingWaterfallRows.filter((row) => row.status === "ready").length, + linked: trackingWaterfallRows.filter((row) => row.status === "linked").length, + }), [trackingWaterfallRows]); + const macroPreviewRows = useMemo(() => { + const rowsWithDifferences = trackingWaterfallRows.filter((row) => row.originalUrl !== row.expandedUrl); + return (rowsWithDifferences.length > 0 ? rowsWithDifferences : trackingWaterfallRows).slice(0, 8); + }, [trackingWaterfallRows]); + const wrapperInspectors = useMemo( + () => buildWrapperInspectors(snapshot.wrapperChain, inventoryAds), + [inventoryAds, snapshot.wrapperChain], + ); + const assetAuditRows = useMemo(() => buildAssetAuditRows(inventoryAds), [inventoryAds]); + const assetAuditSummary = useMemo(() => ({ + total: assetAuditRows.length, + ready: assetAuditRows.filter((row) => row.riskLevel === "ok").length, + review: assetAuditRows.filter((row) => row.riskLevel === "attention").length, + risk: assetAuditRows.filter((row) => row.riskLevel === "risk").length, + }), [assetAuditRows]); + const complianceVerdicts = useMemo( + () => buildComplianceVerdicts( + snapshot.validation !== null, + issues, + inventoryAds, + runtimeInspection, + snapshot.wrapperChain, + assetAuditRows, + ), + [assetAuditRows, inventoryAds, issues, runtimeInspection, snapshot.validation, snapshot.wrapperChain], + ); + const activeComplianceVerdict = complianceVerdicts.find((profile) => profile.id === selectedComplianceProfileId) + ?? complianceVerdicts[0] + ?? null; + const timelineEntries = useMemo(() => { + const sessionEntries = runnerSnapshot.session.events.map((event) => ({ + id: `session-${event.timestamp}-${event.type}`, + at: event.timestamp, + title: event.type, + detail: event.detail ? JSON.stringify(event.detail) : "Session event", + kind: "session" as const, + })); + + const trackingEntries = runnerTracker.tracking.history.map((entry, index) => ({ + id: `tracking-${entry.dispatchedAt}-${String(index)}`, + at: entry.dispatchedAt, + title: `track:${entry.event}`, + detail: `${entry.ok ? "ok" : "failed"} ${entry.url}`, + kind: "tracking" as const, + })); + + return [...runnerTimeline, ...sessionEntries, ...trackingEntries] + .sort((left, right) => new Date(right.at).getTime() - new Date(left.at).getTime()) + .slice(0, 16); + }, [runnerSnapshot.session.events, runnerTimeline, runnerTracker.tracking.history]); + const reportSummary = useMemo( + () => buildReportSummary(lastRun, snapshot, severity, issues, inventoryAds, activeScenario?.label ?? null, lastFix, activeComplianceVerdict), + [activeComplianceVerdict, activeScenario?.label, inventoryAds, issues, lastFix, lastRun, severity, snapshot], + ); + const reportData = useMemo( + () => ({ + generatedAt: new Date().toISOString(), + scenario: activeScenario?.label ?? null, + sourceMode: lastRun.sourceMode, + source: formatRunSource(lastRun), + action: lastRun.action, + status: snapshot.status, + validation: { + version: snapshot.validation?.version ?? null, + valid: snapshot.validation?.summary.valid ?? null, + errors: severity.error, + warnings: severity.warning, + infos: severity.info, + }, + compliance: { + activeProfile: activeComplianceVerdict + ? { + id: activeComplianceVerdict.id, + label: activeComplianceVerdict.label, + status: activeComplianceVerdict.status, + summary: activeComplianceVerdict.summary, + } + : null, + profiles: complianceVerdicts, + }, + wrappers: snapshot.wrapperChain.map((hop) => ({ + index: hop.index, + adType: hop.adType, + adSystem: hop.adSystem, + adTitle: hop.adTitle, + duration: hop.duration, + source: hop.wrapperUri ?? hop.url ?? "inline source", + validation: hop.validation + ? { + errors: hop.validation.summary.errors, + warnings: hop.validation.summary.warnings, + infos: hop.validation.summary.infos, + } + : null, + })), + issues: issues.map((issue) => ({ + id: issue.id, + severity: issue.severity, + location: formatIssueLocation(issue), + message: issue.message, + specRef: issue.spec_ref, + })), + resolvedAds: inventoryAds.map((resolvedAd) => ({ + title: resolvedAd.adTitle || resolvedAd.adPod.adId || "Untitled ad", + adType: resolvedAd.adType, + sequence: resolvedAd.adPod.sequence, + duration: resolvedAd.duration, + mediaFiles: resolvedAd.mediaFiles.map((mediaFile) => ({ + mimeType: mediaFile.mimeType, + delivery: mediaFile.delivery, + width: mediaFile.width, + height: mediaFile.height, + url: mediaFile.url, + })), + })), + assetAudit: assetAuditRows, + fix: lastFix + ? { + applied: lastFix.applied, + remaining: lastFix.remaining.length, + } + : null, + runner: { + status: runnerSnapshot.status, + mediaUrl: runnerMediaUrl, + clickThroughUrl: runnerSnapshot.clickThroughUrl, + muted: runnerSnapshot.muted, + fullscreen: runnerSnapshot.fullscreen, + viewability: runnerSnapshot.viewability, + milestones: runnerSnapshot.milestones, + macroPreset: activeMacroPreset?.id ?? null, + macros: activeMacros, + trackingWaterfall: trackingWaterfallRows, + trackingHistory: runnerTracker.tracking.history, + }, + }), + [activeComplianceVerdict, activeMacroPreset?.id, activeMacros, activeScenario?.label, assetAuditRows, complianceVerdicts, inventoryAds, issues, lastFix, lastRun, runnerMediaUrl, runnerSnapshot.clickThroughUrl, runnerSnapshot.fullscreen, runnerSnapshot.milestones, runnerSnapshot.muted, runnerSnapshot.status, runnerSnapshot.viewability, runnerTracker.tracking.history, severity, snapshot, trackingWaterfallRows], + ); + + useEffect(() => { + if (complianceVerdicts.length === 0) { + return; + } + + if (complianceVerdicts.some((profile) => profile.id === selectedComplianceProfileId)) { + return; + } + + setSelectedComplianceProfileId(complianceVerdicts[0].id); + }, [complianceVerdicts, selectedComplianceProfileId]); + + useEffect(() => { + if (macroPresets.length === 0) { + return; + } + + const selectedPreset = macroPresets.find((preset) => preset.id === selectedMacroPresetId) ?? null; + if (selectedPreset) { + return; + } + + const fallbackPreset = macroPresets[0]; + setSelectedMacroPresetId(fallbackPreset.id); + setMacroEntries(buildMacroEntryDrafts(fallbackPreset.macros)); + }, [macroPresets, selectedMacroPresetId]); + + useEffect(() => { + if (!reportNotice) { + return; + } + + const timeoutId = globalThis.setTimeout(() => { + setReportNotice(null); + }, 2400); + + return () => { + globalThis.clearTimeout(timeoutId); + }; + }, [reportNotice]); + + useEffect(() => { + setRunnerTimeline([]); + runnerProgressBucketRef.current = -1; + }, [runnerSession]); + + useEffect(() => { + setEditorScrollTop(xmlTextareaRef.current?.scrollTop ?? 0); + }, [editorAnnotationsMatchPayload, sourceMode]); + + useEffect(() => { + if (sourceMode !== "xml" || editorAnnotationsStale) { + setSelectedFindingLine(null); + return; + } + + if (selectedFindingLine === null) { + return; + } + + if (!editorIssueMarkers.some((marker) => marker.line === selectedFindingLine)) { + setSelectedFindingLine(null); + } + }, [editorAnnotationsStale, editorIssueMarkers, selectedFindingLine, sourceMode]); + + useEffect(() => { + const video = runnerVideoRef.current; + if (!video) { + return; + } + + if (runnerSnapshot.status === "ended" || runnerSnapshot.status === "error") { + video.pause(); + } + }, [runnerSnapshot.status]); + + const appendRunnerTimeline = (kind: TimelineEntry["kind"], title: string, detail: string) => { + runnerEventCounter.current += 1; + setRunnerTimeline((current) => [ + { + id: `${kind}-${String(runnerEventCounter.current)}`, + at: createTimestamp(), + title, + detail, + kind, + }, + ...current, + ].slice(0, 20)); + }; + + const queueRun = (nextRun: Omit) => { + setLastRun((current) => ({ + id: current.id + 1, + ...nextRun, + })); + }; + + const runAction = (action: ActionMode) => { + const payload = (sourceMode === "xml" ? xmlDraft : urlDraft).trim(); + if (!payload) { + setRunError(sourceMode === "xml" ? "Paste VAST XML before running." : "Enter a VAST URL before running."); + return; + } + + if (sourceMode === "url" && !isValidRemoteUrl(payload)) { + setRunError("Enter a full http:// or https:// VAST URL."); + return; + } + + if (action !== "fix") { + setLastFix(null); + } + + queueRun({ + sourceMode, + action, + payload, + }); + }; + + const loadSample = () => { + setSourceMode("xml"); + setXmlDraft(sampleXml); + setActiveScenarioId(null); + setRunError(null); + setLastFix(null); + }; + + const runScenario = (scenario: ScenarioPreset) => { + const payload = scenario.sourceMode === "url" ? buildScenarioUrl(scenario.payload) : absolutizeScenarioXmlLocalUrls(scenario.payload); + setActiveScenarioId(scenario.id); + setRunError(null); + setLastFix(null); + setSourceMode(scenario.sourceMode); + + if (scenario.sourceMode === "xml") { + setXmlDraft(payload); + } else { + setUrlDraft(payload); + } + + queueRun({ + sourceMode: scenario.sourceMode, + action: scenario.action, + payload, + }); + }; + + const applyFixedXml = () => { + if (!lastFix) { + return; + } + + const nextPayload = lastFix.xml; + setSourceMode("xml"); + setXmlDraft(nextPayload); + setActiveScenarioId(null); + setRunError(null); + queueRun({ + sourceMode: "xml", + action: "validate", + payload: nextPayload, + }); + }; + + const copyReportSummary = async () => { + try { + await globalThis.navigator.clipboard.writeText(reportSummary); + setReportNotice("Summary copied to clipboard."); + } catch { + setReportNotice("Clipboard access unavailable. Use a download instead."); + } + }; + + const copyErrorFindings = async () => { + try { + await globalThis.navigator.clipboard.writeText( + buildErrorClipboardText(lastRun, activeScenario?.label ?? null, activeComplianceVerdict, issues), + ); + setReportNotice( + issues.some((issue) => issue.severity === "error") + ? "Error findings copied to clipboard." + : "No error findings. Status note copied to clipboard.", + ); + } catch { + setReportNotice("Error export copy failed. Clipboard access unavailable."); + } + }; + + const copyShareLink = async () => { + try { + const sharePayload = encodeBase64Url(JSON.stringify({ + sourceMode: lastRun.sourceMode, + action: lastRun.action, + payload: lastRun.payload, + activeScenarioId, + selectedComplianceProfileId, + } satisfies SharedSessionState)); + const shareUrl = new URL(globalThis.location.href); + shareUrl.hash = new URLSearchParams({ session: sharePayload }).toString(); + await globalThis.navigator.clipboard.writeText(shareUrl.toString()); + setReportNotice("Share link copied to clipboard."); + } catch { + setReportNotice("Share link copy failed. Clipboard access unavailable."); + } + }; + + const downloadReport = (kind: "txt" | "json") => { + const content = kind === "json" ? JSON.stringify(reportData, null, 2) : reportSummary; + const blob = new Blob([content], { + type: kind === "json" ? "application/json;charset=utf-8" : "text/plain;charset=utf-8", + }); + const objectUrl = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = objectUrl; + anchor.download = `vast-validator-report-${activeScenario?.id ?? "custom"}.${kind}`; + document.body.append(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(objectUrl); + setReportNotice(kind === "json" ? "JSON report downloaded." : "Text report downloaded."); + }; + + const downloadArtifactBundle = async () => { + const bundleBaseName = `vast-validator-artifacts-${activeScenario?.id ?? "custom"}`; + const zip = new JSZip(); + + zip.file( + "README.txt", + buildArtifactReadme( + activeScenario?.label ?? null, + activeMacroPreset, + activeComplianceVerdict, + trackingWaterfallRows.length, + timelineEntries.length, + assetAuditRows.length, + ), + ); + zip.file("report.txt", reportSummary); + zip.file("report.json", JSON.stringify(reportData, null, 2)); + + if (lastRun.sourceMode === "xml") { + zip.file("source/request.xml", lastRun.payload); + } else { + zip.file("source/request-url.txt", `${lastRun.payload}\n`); + } + + if (snapshot.rootXml) { + zip.file("source/root.xml", snapshot.rootXml); + } + + if (lastFix?.xml) { + zip.file("source/fixed.xml", lastFix.xml); + } + + zip.file("runtime/macros.json", JSON.stringify({ + preset: activeMacroPreset?.id ?? null, + macros: activeMacros, + }, null, 2)); + zip.file("runtime/tracking-waterfall.json", JSON.stringify(trackingWaterfallRows, null, 2)); + zip.file("runtime/tracking-history.json", JSON.stringify(runnerTracker.tracking.history, null, 2)); + zip.file("runtime/timeline.json", JSON.stringify(timelineEntries, null, 2)); + zip.file("runtime/playback.json", JSON.stringify({ + status: runnerSnapshot.status, + mediaUrl: runnerMediaUrl, + clickThroughUrl: runnerSnapshot.clickThroughUrl, + currentTimeSec: runnerSnapshot.currentTimeSec, + durationSec: runnerSnapshot.durationSec, + muted: runnerSnapshot.muted, + fullscreen: runnerSnapshot.fullscreen, + viewability: runnerSnapshot.viewability, + milestones: runnerSnapshot.milestones, + }, null, 2)); + zip.file("validation/wrapper-chain.json", JSON.stringify(snapshot.wrapperChain, null, 2)); + zip.file("validation/resolved-ads.json", JSON.stringify(inventoryAds, null, 2)); + zip.file("validation/issues.json", JSON.stringify(issues, null, 2)); + zip.file("validation/compliance.json", JSON.stringify({ + activeProfile: activeComplianceVerdict, + profiles: complianceVerdicts, + }, null, 2)); + zip.file("validation/asset-audit.json", JSON.stringify(assetAuditRows, null, 2)); + + const blob = await zip.generateAsync({ type: "blob" }); + const objectUrl = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = objectUrl; + anchor.download = `${bundleBaseName}.zip`; + document.body.append(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(objectUrl); + setReportNotice("Artifact bundle downloaded."); + }; + + const applyMacroPreset = (presetId: string) => { + const preset = macroPresets.find((candidate) => candidate.id === presetId) ?? null; + if (!preset) { + return; + } + + setSelectedMacroPresetId(presetId); + setMacroEntries(buildMacroEntryDrafts(preset.macros)); + }; + + const updateMacroEntry = (id: string, field: "key" | "value", value: string) => { + setMacroEntries((current) => current.map((entry) => { + if (entry.id !== id) { + return entry; + } + + return { + ...entry, + [field]: field === "key" ? value.toUpperCase() : value, + }; + })); + }; + + const addMacroEntry = () => { + macroEntryCounterRef.current += 1; + setMacroEntries((current) => [ + ...current, + { + id: `macro-${String(macroEntryCounterRef.current)}`, + key: "", + value: "", + }, + ]); + }; + + const removeMacroEntry = (id: string) => { + setMacroEntries((current) => { + if (current.length === 1) { + return current.map((entry) => entry.id === id ? { ...entry, key: "", value: "" } : entry); + } + + return current.filter((entry) => entry.id !== id); + }); + }; + + const preparePlaybackRunner = async () => { + try { + const prepared = await playback.initialize(); + appendRunnerTimeline( + "ui", + "runner:prepare", + prepared.mediaSelection.selected + ? `Prepared ${prepared.mediaSelection.selected.mimeType} media.` + : "Prepared session with no playable media selection.", + ); + } catch (error) { + appendRunnerTimeline("ui", "runner:error", error instanceof Error ? error.message : String(error)); + } + }; + + const setPlaybackViewability = async (viewability: VastPlaybackViewability) => { + try { + await playback.setViewability(viewability); + appendRunnerTimeline("ui", `viewability:${viewability}`, "Updated playback viewability."); + } catch (error) { + appendRunnerTimeline("ui", "viewability:error", error instanceof Error ? error.message : String(error)); + } + }; + + const triggerRunnerClick = async () => { + try { + const result = await playback.click(); + appendRunnerTimeline( + "ui", + "runner:click", + result.clickThroughUrl ? `Tracked click-through for ${result.clickThroughUrl}` : "Tracked click without click-through URL.", + ); + } catch (error) { + appendRunnerTimeline("ui", "runner:click-error", error instanceof Error ? error.message : String(error)); + } + }; + + const skipPlayback = async () => { + try { + await playback.skip(); + runnerVideoRef.current?.pause(); + appendRunnerTimeline("ui", "runner:skip", "Marked the current ad as skipped."); + } catch (error) { + appendRunnerTimeline("ui", "runner:skip-error", error instanceof Error ? error.message : String(error)); + } + }; + + const signalRunnerError = async () => { + try { + await playback.signalError({ macros: activeMacros }); + runnerVideoRef.current?.pause(); + appendRunnerTimeline("ui", "runner:signal-error", "Tracked an error against the current playback session."); + } catch (error) { + appendRunnerTimeline("ui", "runner:error", error instanceof Error ? error.message : String(error)); + } + }; + + const toggleRunnerMute = async () => { + const video = runnerVideoRef.current; + const nextMuted = !(video?.muted ?? runnerSnapshot.muted); + if (video) { + video.muted = nextMuted; + } + + try { + await playback.setMuted(nextMuted); + appendRunnerTimeline("ui", nextMuted ? "runner:mute" : "runner:unmute", `Muted=${String(nextMuted)}.`); + } catch (error) { + appendRunnerTimeline("ui", "runner:mute-error", error instanceof Error ? error.message : String(error)); + } + }; + + const syncRunnerMuted = async () => { + const video = runnerVideoRef.current; + const nextMuted = video?.muted ?? runnerSnapshot.muted; + + try { + await playback.setMuted(nextMuted); + } catch (error) { + appendRunnerTimeline("media", "video:mute-sync-error", error instanceof Error ? error.message : String(error)); + } + }; + + const handleRunnerPlay = async () => { + try { + if (runnerSnapshot.status === "paused") { + await playback.resume(); + appendRunnerTimeline("media", "video:resume", "Resumed the media element."); + return; + } + + if (!runnerSnapshot.milestones.start) { + await playback.start(); + appendRunnerTimeline("media", "video:start", "Started playback and dispatched impression/start tracking."); + } + } catch (error) { + appendRunnerTimeline("media", "video:error", error instanceof Error ? error.message : String(error)); + } + }; + + const handleRunnerPause = async () => { + const video = runnerVideoRef.current; + if (video?.ended || runnerSnapshot.status !== "playing") { + return; + } + + try { + await playback.pause(); + appendRunnerTimeline("media", "video:pause", "Paused the media element."); + } catch (error) { + appendRunnerTimeline("media", "video:pause-error", error instanceof Error ? error.message : String(error)); + } + }; + + const handleRunnerTimeUpdate = async () => { + const video = runnerVideoRef.current; + if (!video) { + return; + } + + const bucket = Math.floor(video.currentTime * 4); + if (bucket === runnerProgressBucketRef.current) { + return; + } + + runnerProgressBucketRef.current = bucket; + + try { + await playback.updateProgress(video.currentTime, Number.isFinite(video.duration) ? video.duration : undefined); + } catch (error) { + appendRunnerTimeline("media", "video:progress-error", error instanceof Error ? error.message : String(error)); + } + }; + + const handleRunnerEnded = async () => { + try { + await playback.complete(); + appendRunnerTimeline("media", "video:ended", "Completed playback for the current ad."); + } catch (error) { + appendRunnerTimeline("media", "video:end-error", error instanceof Error ? error.message : String(error)); + } + }; + + const focusFindingsForLine = (line: number) => { + setSelectedFindingLine((current) => { + const nextLine = current === line ? null : line; + if (nextLine !== null) { + globalThis.requestAnimationFrame(() => { + findingsSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); + } + + return nextLine; + }); + }; + + return ( +
+
+
+ Independent fork + Next-Gen VAST Tester +
+ Built on the legacy IAB Tech Lab tester foundation and extended for modern VAST QA workflows. +
+
+
+ Coverage + VAST 2.0-4.3 +
+
+ Runtime + vastlint / vastlint-client +
+
+ Interface + Standalone forked workbench +
+
+
+ Forked from the legacy IAB Tech Lab VAST Tester workflow. + + This build keeps the standalone tester identity, preserves the flatter review-oriented UI, and pushes the concept forward with vastlint-powered validation, deterministic repair, wrapper inspection, runtime QA, and partner-shareable diagnostics. + +
+
+
+

Independent evolution

+

Next-Gen VAST Tester

+

+ Built on the foundations of the legacy IAB Tech Lab VAST Tester and expanded into a separate vastlint-powered QA workbench for validation, repair, wrappers, playback, tracking, and shareable review. +

+
+
+ Current run + {lastRun.sourceMode === "xml" ? "Editor XML" : "Remote URL"} + {lastRun.action} + Status: {snapshot.status} +
+
+ +
+
+
+
+

Source

+

Validation request

+
+
+ + +
+
+ +
+
+

Scenario library

+ Version-aware presets grouped by baseline, creative, measurement, and CTV QA coverage. +
+
+
+ + Showing {String(filteredScenarioCount)} of {String(SCENARIO_PRESETS.length)} presets + + + {activeScenario !== null && !activeScenarioMatchesFilters + ? `Active scenario \"${activeScenario.label}\" is outside the current filter slice.` + : "Load and run a canned demo in one click."} + +
+
+ + + + +
+
+
+ {filteredScenarioGroups.map((group) => ( +
+
+

{group.label}

+ {group.description} +
+
+ {group.scenarios.map((scenario) => ( + + ))} +
+
+ ))} + {filteredScenarioCount === 0 ? ( +
No presets match the current version, action, and surface filters.
+ ) : null} +
+
+ + {sourceMode === "xml" ? ( +