[
  {
    "path": ".dockerignore",
    "content": "**/node_modules/\nbuild/\n**/.env\n**/.env.local\nDockerfile\ndocker-compose.yml\napi/extensions/**/dist\n"
  },
  {
    "path": ".env.example",
    "content": "VITE_API_URL=http://HOST:3000\nVITE_WS_URL=ws://HOST:3000"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto\n*.sh text eol=lf\n*.conf text eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"[BUG] TITLE\"\nlabels: ''\nassignees: ''\n\n---\n\nIssue tracker is **ONLY** used for reporting bugs. New features should be discussed in our [Discord server](https://discord.gg/steel-dev).\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Browser [e.g. chrome, safari]\n - Version [e.g. 22]\n\n**Smartphone (please complete the following information):**\n - Device: [e.g. iPhone6]\n - OS: [e.g. iOS8.1]\n - Browser [e.g. stock browser, safari]\n - Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 💬 Discord Community\n    url: https://discord.gg/steel-dev\n    about: Join our Discord server for real-time help, discussions, and community support\n  - name: 📚 Documentation\n    url: https://docs.steel.dev/\n    about: Check our comprehensive documentation for guides and API reference\n  - name: 🍳 Steel Cookbook\n    url: https://github.com/steel-dev/steel-cookbook\n    about: Browse code examples and common use cases\n  - name: 🔒 Security Issues\n    url: mailto:security@steel.dev\n    about: Please report security vulnerabilities privately via email\n  - name: 🤖 Automated Issue Creation\n    url: https://github.com/steel-dev/steel-browser/issues/new/choose\n    about: Use our templates for better issue tracking "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for Steel Browser\ntitle: \"[FEATURE] \"\nlabels: 'enhancement'\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Use case**\nDescribe the specific use case or scenario where this feature would be helpful.\n\n**Implementation ideas (optional)**\nIf you have ideas about how this could be implemented, please share them.\n\n**Additional context**\nAdd any other context, screenshots, or examples about the feature request here.\n\n**Would you be willing to contribute this feature?**\n- [ ] Yes, I'd like to work on this\n- [ ] Yes, with guidance from maintainers\n- [ ] No, but I'd be happy to test it\n- [ ] No, just suggesting the idea "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "content": "---\nname: Question\nabout: Ask a question about using Steel Browser\ntitle: \"[QUESTION] \"\nlabels: 'question'\nassignees: ''\n\n---\n\n**Before asking your question**\n- [ ] I've checked the [documentation](https://docs.steel.dev/)\n- [ ] I've searched existing issues\n- [ ] I've looked at the [Steel Cookbook](https://github.com/steel-dev/steel-cookbook) for examples\n\n**What are you trying to do?**\nA clear description of what you're trying to accomplish.\n\n**What have you tried?**\nDescribe what you've already attempted and what happened.\n\n**Code example (if applicable)**\n```typescript\n// Include relevant code snippets here\n```\n\n**Environment**\n- Steel Browser version: \n- Node.js version:\n- Operating System:\n- Browser: Chrome/Chromium version\n\n**Additional context**\nAdd any other context, error messages, or screenshots that might help.\n\n---\n\n💡 **Tip**: For real-time help and community discussion, consider joining our [Discord server](https://discord.gg/steel-dev)! "
  },
  {
    "path": ".github/auto-assign.yml",
    "content": "# Auto-assign configuration for Steel Browser\n# This file is used by the kentaro-m/auto-assign-action\n\n# Add reviewers to pull requests\naddReviewers: true\n\n# Add assignees to pull requests  \naddAssignees: false\n\n# Number of reviewers to add to each pull request\nnumberOfReviewers: 1\n\n# Reviewers to be added to pull requests (GitHub usernames)\nreviewers:\n  - fukouda\n\n# Skip draft pull requests\nskipDraftPR: true\n\n# Only assign reviewers if no reviewers are already assigned\nassignWhenNoReviewers: true "
  },
  {
    "path": ".github/labeler.yml",
    "content": "# GitHub Labeler Configuration for Steel Browser\n# This file is used by the actions/labeler@v5 action in pr-checks.yml\n\n# Component labels based on file paths\napi:\n- changed-files:\n  - any-glob-to-any-file: 'api/**/*'\n  \nui:\n- changed-files:\n  - any-glob-to-any-file: 'ui/**/*'\n  \ndocumentation:\n- changed-files:\n  - any-glob-to-any-file: \n    - 'docs/**/*'\n    - '*.md'\n    - 'README.md'\n    - 'CONTRIBUTING.md'\n  \nplugin-system:\n- changed-files:\n  - any-glob-to-any-file: 'api/src/services/cdp/plugins/**/*'\n  \ncdp:\n- changed-files:\n  - any-glob-to-any-file:\n    - 'api/src/services/cdp/**/*'\n    - 'api/src/modules/cdp/**/*'\n  \nsession-management:\n- changed-files:\n  - any-glob-to-any-file:\n    - 'api/src/services/session.service.ts'\n    - 'api/src/modules/sessions/**/*'\n  \nfile-storage:\n- changed-files:\n  - any-glob-to-any-file:\n    - 'api/src/services/file.service.ts'\n    - 'api/src/modules/files/**/*'\n\ndocker:\n- changed-files:\n  - any-glob-to-any-file:\n    - 'docker-compose*.yml'\n    - 'api/Dockerfile'\n    - 'ui/Dockerfile'\n    - '.dockerignore'\n\ndependencies:\n- changed-files:\n  - any-glob-to-any-file:\n    - 'package-lock.json'\n    - '**/package-lock.json'\n    - 'package.json'\n    - '**/package.json'\n\ngithub-actions:\n- changed-files:\n  - any-glob-to-any-file:\n    - '.github/workflows/**/*'\n    - '.github/**/*'\n\ntesting:\n- changed-files:\n  - any-glob-to-any-file:\n    - '**/*.test.ts'\n    - '**/*.test.js'\n    - '**/*.spec.ts'\n    - '**/*.spec.js'\n    - 'tsconfig.test.json' "
  },
  {
    "path": ".github/labels.yml",
    "content": "# GitHub Labels Configuration for Steel Browser\n# This file can be used with the github-labels CLI tool to sync labels\n\n# Type labels\n- name: \"bug\"\n  color: \"d73a4a\"\n  description: \"Something isn't working\"\n\n- name: \"enhancement\"\n  color: \"a2eeef\"\n  description: \"New feature or request\"\n\n- name: \"documentation\"\n  color: \"0075ca\"\n  description: \"Improvements or additions to documentation\"\n\n- name: \"question\"\n  color: \"d876e3\"\n  description: \"Further information is requested\"\n\n# Priority labels\n- name: \"priority: critical\"\n  color: \"b60205\"\n  description: \"Critical issue that needs immediate attention\"\n\n- name: \"priority: high\"\n  color: \"d93f0b\"\n  description: \"High priority issue\"\n\n- name: \"priority: medium\"\n  color: \"fbca04\"\n  description: \"Medium priority issue\"\n\n- name: \"priority: low\"\n  color: \"0e8a16\"\n  description: \"Low priority issue\"\n\n# Difficulty labels\n- name: \"good first issue\"\n  color: \"7057ff\"\n  description: \"Good for newcomers\"\n\n- name: \"help wanted\"\n  color: \"008672\"\n  description: \"Extra attention is needed\"\n\n- name: \"difficulty: easy\"\n  color: \"c2e0c6\"\n  description: \"Easy to implement\"\n\n- name: \"difficulty: medium\"\n  color: \"fef2c0\"\n  description: \"Moderate difficulty\"\n\n- name: \"difficulty: hard\"\n  color: \"f9d0c4\"\n  description: \"Hard to implement\"\n\n# Component labels\n- name: \"api\"\n  color: \"1f77b4\"\n  description: \"Related to the API backend\"\n\n- name: \"ui\"\n  color: \"ff7f0e\"\n  description: \"Related to the frontend UI\"\n\n- name: \"plugin-system\"\n  color: \"2ca02c\"\n  description: \"Related to the plugin architecture\"\n\n- name: \"cdp\"\n  color: \"d62728\"\n  description: \"Related to Chrome DevTools Protocol\"\n\n- name: \"session-management\"\n  color: \"9467bd\"\n  description: \"Related to browser session handling\"\n\n- name: \"file-storage\"\n  color: \"8c564b\"\n  description: \"Related to file upload/download functionality\"\n\n- name: \"docker\"\n  color: \"0db7ed\"\n  description: \"Related to Docker configuration\"\n\n- name: \"testing\"\n  color: \"17becf\"\n  description: \"Related to testing infrastructure\"\n\n# Status labels\n- name: \"status: blocked\"\n  color: \"b60205\"\n  description: \"Blocked by another issue or external dependency\"\n\n- name: \"status: in progress\"\n  color: \"fbca04\"\n  description: \"Currently being worked on\"\n\n- name: \"status: needs review\"\n  color: \"0052cc\"\n  description: \"Needs code review\"\n\n- name: \"status: needs testing\"\n  color: \"1d76db\"\n  description: \"Needs testing before merge\"\n\n- name: \"status: ready to merge\"\n  color: \"0e8a16\"\n  description: \"Ready to be merged\"\n\n# Breaking change labels\n- name: \"breaking change\"\n  color: \"b60205\"\n  description: \"Introduces breaking changes\"\n\n- name: \"backwards compatible\"\n  color: \"0e8a16\"\n  description: \"Backwards compatible changes\"\n\n# Special labels\n- name: \"duplicate\"\n  color: \"cfd3d7\"\n  description: \"This issue or pull request already exists\"\n\n- name: \"invalid\"\n  color: \"e4e669\"\n  description: \"This doesn't seem right\"\n\n- name: \"wontfix\"\n  color: \"ffffff\"\n  description: \"This will not be worked on\"\n\n- name: \"dependencies\"\n  color: \"0366d6\"\n  description: \"Pull requests that update a dependency file\"\n\n- name: \"security\"\n  color: \"d73a4a\"\n  description: \"Security-related issue\"\n\n- name: \"performance\"\n  color: \"ff9500\"\n  description: \"Performance improvement\"\n\n- name: \"refactor\"\n  color: \"5319e7\"\n  description: \"Code refactoring\"\n\n- name: \"chore\"\n  color: \"fef2c0\"\n  description: \"Maintenance tasks\" "
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Description\n\nBrief description of the changes in this PR.\n\n## Type of Change\n\n- [ ] Bug fix (non-breaking change which fixes an issue)\n- [ ] New feature (non-breaking change which adds functionality)\n- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)\n- [ ] Documentation update\n- [ ] Code refactoring\n- [ ] Performance improvement\n- [ ] Test addition/update\n\n## Related Issues\n\nCloses #(issue number)\nRelated to #(issue number)\n\n## Changes Made\n\n- [ ] List specific changes made\n- [ ] Include any new files or major modifications\n- [ ] Mention any removed functionality\n\n## Testing\n\n- [ ] I have tested this locally\n- [ ] I have added/updated unit tests\n- [ ] I have added/updated integration tests\n- [ ] I have tested with Docker\n- [ ] All existing tests pass\n\n## Documentation\n\n- [ ] I have updated relevant documentation\n- [ ] I have added JSDoc comments for new public APIs\n- [ ] I have updated the README if needed\n- [ ] I have updated the CHANGELOG if needed\n\n## Code Quality\n\n- [ ] My code follows the project's style guidelines\n- [ ] I have performed a self-review of my code\n- [ ] I have commented my code, particularly in hard-to-understand areas\n- [ ] My changes generate no new warnings or errors\n\n## Breaking Changes\n\nIf this is a breaking change, please describe:\n\n1. What breaks:\n2. How users should migrate:\n3. Why this change is necessary:\n\n## Screenshots (if applicable)\n\nInclude screenshots or GIFs for UI changes.\n\n## Additional Notes\n\nAny additional information, concerns, or context for reviewers.\n\n---\n\n## Reviewer Checklist\n\n- [ ] Code follows project conventions and style\n- [ ] Changes are well-tested\n- [ ] Documentation is updated appropriately\n- [ ] No security concerns\n- [ ] Performance impact is acceptable\n- [ ] Breaking changes are properly documented "
  },
  {
    "path": ".github/workflows/auto-assign.yml",
    "content": "# .github/workflows/auto-assign.yml\nname: Auto Assign\n\non:\n  pull_request:\n    types: [opened, ready_for_review]\n\njobs:\n  assign:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Auto Assign\n        uses: kentaro-m/auto-assign-action@v1.2.5\n        with:\n          configuration-path: \".github/auto-assign.yml\"\n"
  },
  {
    "path": ".github/workflows/build-docker.yml",
    "content": "name: Build and Push Latest Docker Image to GHCR\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  push_to_registry:\n    name: Build and Push Latest Docker Image to GHCR\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ secrets.GH_USERNAME }}\n          password: ${{ secrets.GH_TOKEN }}\n\n      - name: Build and push the latest Steel Browser image\n        run: |\n          docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/steel-dev/steel-browser:latest .\n\n      - name: Build and push the latest Steel Browser API image\n        run: |\n          docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/steel-dev/steel-browser-api:latest . -f ./api/Dockerfile\n\n      - name: Build and push the latest Steel Browser UI image\n        run: |\n          docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/steel-dev/steel-browser-ui:latest . -f ./ui/Dockerfile\n"
  },
  {
    "path": ".github/workflows/check-build.yml",
    "content": "name: Check Docker Build\n\non:\n  pull_request:\n    branches:\n      - main\n\njobs:\n  check-docker-build:\n    name: Check Docker Build\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        id: buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Build the latest Steel Browser image\n        run: |\n          docker build -t steel-browser -f ./Dockerfile .\n\n      - name: Build the latest Steel Browser API image\n        run: |\n          docker build -t steel-browser-api -f ./api/Dockerfile .\n      - name: Build the latest Steel Browser UI image\n        run: |\n          docker build -t steel-browser-ui -f ./ui/Dockerfile .\n"
  },
  {
    "path": ".github/workflows/pr-checks.yml",
    "content": "# .github/workflows/pr-checks.yml\nname: PR Quality Checks\n\non:\n  pull_request_target:\n    branches: [main]\n\npermissions:\n  contents: read\n  pull-requests: write\n\njobs:\n  validate-pr:\n    runs-on: ubuntu-latest\n    steps:\n      # Check PR title follows conventional commits\n      - name: Validate PR Title\n        uses: amannn/action-semantic-pull-request@v5\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      # Auto-assign labels based on files changed\n      - name: Label PR\n        uses: actions/labeler@v5\n\n      # Check for breaking changes\n      - name: Check Breaking Changes\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          # Get list of changed files in this PR\n          CHANGED_FILES=$(gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' | tr '\\n' ' ')\n          echo \"Changed files: $CHANGED_FILES\"\n\n          # Check for potential breaking changes in specific files\n          if echo \"$CHANGED_FILES\" | grep -E \"(api/src/types/|api/src/steel-browser-plugin.ts|api/src/services/cdp/plugins/core/)\"; then\n            echo \"::warning::Potential breaking changes detected in core APIs\"\n          fi\n\n          # Check for package.json changes\n          if echo \"$CHANGED_FILES\" | grep -E \"package\\.json$\"; then\n            echo \"::warning::Package.json changes detected - review dependencies carefully\"\n          fi\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Automatic Release\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      packages: write\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      # Automatic semantic version bump (major/minor/patch from commit messages)\n      - name: Bump version and push tag\n        id: bump_version\n        uses: phips28/gh-action-bump-version@v11.0.3\n        with:\n          tag-prefix: \"v\"\n          tag-suffix: \"-beta\"\n          skip-commit: true\n          patch-wording: \"patch,fix,fixes,docs,feat,feature,minor\"\n          minor-wording: \"\"\n          major-wording: \"breaking,breaking-change,major\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      # Generate changelog file from previous commits\n      - name: Generate changelog\n        uses: mikepenz/release-changelog-builder-action@v5\n        id: changelog\n        with:\n          mode: \"COMMIT\"\n          configurationJson: |\n            {\n                \"template\": \"#{{CHANGELOG}}\",\n                \"commit_template\": \"- [`#{{MERGE_SHA_SUBSTRING}}`](${{ github.server_url }}/${{ github.repository }}/commit/#{{MERGE_SHA}}): #{{TITLE}} (@#{{AUTHOR}})\",\n                \"custom_placeholders\": [\n                    {\n                        \"name\": \"MERGE_SHA_SUBSTRING\",\n                        \"source\": \"MERGE_SHA\",\n                        \"transformer\": {\n                            \"pattern\": \"^(.{6})\",\n                            \"method\": \"regexr\",\n                            \"target\": \"$1\"\n                        }\n                    }\n                ],\n                \"categories\": [\n                    {\n                        \"title\": \"## Improvements\",\n                        \"labels\": [\n                            \"feat\",\n                            \"feature\"\n                        ]\n                    },\n                    {\n                        \"title\": \"## Bug Fixes\",\n                        \"labels\": [\n                            \"fix\",\n                            \"bug\"\n                        ]\n                    },\n                    {\n                        \"title\": \"## Documentation\",\n                        \"labels\": [\n                            \"docs\"\n                        ]\n                    },\n                    {\n                        \"title\": \"## Housekeeping\",\n                        \"labels\": []\n                    }\n                ],\n                \"sort\": {\n                    \"order\": \"ASC\",\n                    \"on_property\": \"mergedAt\"\n                },\n                \"label_extractor\": [\n                    {\n                        \"pattern\": \"^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\\\([\\\\w\\\\-\\\\.]+\\\\))?(!)?: ([\\\\w ])+([\\\\s\\\\S]*)\",\n                        \"on_property\": \"title\",\n                        \"target\": \"$1\"\n                    }\n                ]\n            }\n          toTag: ${{ steps.bump_version.outputs.newTag }}\n          fromTag: \"\"\n\n      # Create automatic GitHub release\n      - name: Create GitHub Release\n        uses: ncipollo/release-action@v1.18.0\n        with:\n          token: \"${{ secrets.GITHUB_TOKEN }}\"\n          tag: ${{ steps.bump_version.outputs.newTag }}\n          prerelease: false\n          name: \"Release ${{ steps.bump_version.outputs.newTag }}\"\n          body: |\n            ${{ steps.changelog.outputs.changelog }}\n\n            ---\n\n            ![release-image](https://raw.githubusercontent.com/steel-dev/.github/refs/heads/main/profile/github_hero.png)\n\n            ## Come Hang Out\n            - Questions? Join us on [Discord](https://discord.gg/gPpvhNvc5R)\n            - Found a bug? Open an issue on [GitHub](https://github.com/steel-dev/steel-browser/issues)\n"
  },
  {
    "path": ".github/workflows/welcome.yml",
    "content": "# .github/workflows/welcome.yml\nname: Welcome\n\non:\n  issues:\n    types: [opened]\n  pull_request:\n    types: [opened]\n\njobs:\n  welcome:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Welcome new contributors\n        uses: actions/first-interaction@v1\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n          issue-message: |\n            Thanks for opening your first issue! 🎉 \n            Please check our [Contributing Guide](CONTRIBUTING.md) and [Troubleshooting Guide](docs/TROUBLESHOOTING.md).\n          pr-message: |\n            Thanks for your first contribution! 🚀 \n            Please ensure you've followed our [Contributing Guidelines](CONTRIBUTING.md).\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.pnp\n.pnp.js\ncoverage\n.DS_Store\n*.pem\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n.env\n.env.local\n.env.development.local\n.test.env.local\n.env.production.local\n!.env.example\nproduction.env\n.turbo\nbuild\ndb/data\n*.tsbuildinfo\nout\n.idea\n*.env\ndist\n.aider*\nextensions/*\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "npx --no-install commitlint --edit \"$1\""
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\n# Code formatting and linting\necho \"🎨 Running code formatting...\"\nnpm run pretty -w api\n\necho \"🔍 Running linting...\"\nnpm run lint -w ui --fix\n\n# Type checking\necho \"🔧 Running type checking...\"\nnpm run build\n\n# Add formatted files back to staging\ngit add -u\n\necho \"✅ Pre-commit checks passed!\" "
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Steel Browser\n\nWelcome to Steel Browser! 🎉 We're excited that you're interested in contributing to our open-source browser API. This guide will help you get started and make your first contribution.\n\n## 🚀 Quick Start\n\n### Prerequisites\n\n- **Node.js**: Version 22 or higher\n- **npm**: Version 10 or higher  \n- **Docker**: For containerized development (optional but recommended)\n- **Git**: For version control\n- **Chrome/Chromium**: Required for browser automation\n\n### Development Setup\n\n1. **Fork and Clone**\n   ```bash\n   git clone https://github.com/steel-dev/steel-browser.git\n   cd steel-browser\n   ```\n\n2. **Install Dependencies**\n   ```bash\n   npm install\n   ```\n\n3. **Start Development Environment**\n   ```bash\n   # Start both API and UI in development mode\n   npm run dev\n   \n   # Or start individually:\n   npm run dev -w api    # API server on http://localhost:3000\n   npm run dev -w ui     # UI server on http://localhost:5173\n   ```\n\n4. **Verify Setup**\n   - API: Visit http://localhost:3000/documentation\n   - UI: Visit http://localhost:5173\n   - Test REPL: `cd repl && npm start`\n\n### Docker Development (Alternative)\n\n```bash\n# Build and run with Docker Compose\ndocker-compose -f docker-compose.dev.yml up --build\n\n# Or use production images\ndocker-compose up\n```\n\n## 📁 Project Structure\n\n```\nsteel-browser/\n├── api/                    # Backend API (Fastify + Puppeteer)\n│   ├── src/\n│   │   ├── modules/        # API modules (actions, sessions, etc.)\n│   │   ├── plugins/        # Fastify plugins\n│   │   ├── services/       # Core services (CDP, file, session)\n│   │   └── types/          # TypeScript type definitions\n│   └── extensions/         # Browser extensions (must pass name as param to session creation)\n├── ui/                     # Frontend UI (React + Vite)\n│   └── src/\n│       ├── components/     # Reusable UI components\n│       ├── containers/     # Page containers\n│       └── contexts/       # React contexts\n├── repl/                   # Interactive REPL for testing\n└── docs/                   # Documentation\n```\n\n## 🏗️ Architecture Overview\n\nSteel Browser follows a plugin-based architecture:\n\n### Core Components\n\n1. **Steel Browser Plugin** (`api/src/steel-browser-plugin.ts`)\n   - Registers all the necessary services, routes, and hooks\n   - Can be used as a standalone plugin or integrated into your own application\n   - Provides the core functionality of Steel Browser\n\n2. **CDP Service** (`api/src/services/cdp/cdp.service.ts`)\n   - Manages Chrome DevTools Protocol connections\n   - Handles browser lifecycle and page management\n   - Supports plugin system for extensibility\n\n2. **CDP Plugin System** (`api/src/services/cdp/plugins/`)\n   - **BasePlugin**: Abstract base class for all plugins\n   - **PluginManager**: Manages plugin lifecycle and events\n   - Plugins can hook into browser events (launch, page creation, navigation, etc.)\n\n3. **Session Management** (`api/src/services/session.service.ts`)\n   - Manages browser sessions and their state\n   - Handles session persistence and cleanup\n\n4. **File Storage** (`api/src/services/file.service.ts`)\n   - Manages file uploads, downloads, and storage\n   - Supports session-scoped file management\n\n\n### Using Steel Browser as a Plugin\n\n```typescript\nimport Fastify from 'fastify';\nimport steelBrowserPlugin, { SteelBrowserConfig } from './api/src/steel-browser-plugin.js';\n\nconst fastify = Fastify({ logger: true });\n\n// Register Steel Browser plugin with configuration\nconst config: SteelBrowserConfig = {\n  fileStorage: {\n    maxSizePerSession: 100 * 1024 * 1024, // 100MB\n  },\n  customWsHandlers: [\n    // Your custom WebSocket handlers\n  ],\n};\n\nawait fastify.register(steelBrowserPlugin, config);\n\n// Your additional routes and plugins\nawait fastify.register(myCustomPlugin);\n\nawait fastify.listen({ port: 3000 });\n```\n\n### Configuration Options\n\nThe `SteelBrowserConfig` interface allows you to customize:\n\n- **fileStorage**: Configure file storage limits per session\n- **customWsHandlers**: Add custom WebSocket handlers for real-time features\n\n### CDP Plugin Development\n\nUsing the CDP Plugin System, you can create plugins that hook into browser lifecycle events:\n\n```typescript\nimport { BasePlugin, PluginOptions } from './api/src/services/cdp/plugins/core/base-plugin.js';\nimport { Browser, Page } from 'puppeteer-core';\n\nexport class MyCustomPlugin extends BasePlugin {\n  constructor(options: PluginOptions) {\n    super({ name: 'my-custom-plugin', ...options });\n  }\n\n  async onBrowserLaunch(browser: Browser): Promise<void> {\n    this.cdpService?.logger.info('Custom plugin: Browser launched');\n    // Your custom logic here\n  }\n\n  async onPageCreated(page: Page): Promise<void> {\n    this.cdpService?.logger.info('Custom plugin: New page created');\n    // Handle new page creation\n    await page.setUserAgent('MyCustomBot/1.0');\n  }\n\n  async onPageNavigate(page: Page): Promise<void> {\n    // Handle page navigation\n    const url = page.url();\n    this.cdpService?.logger.info(`Custom plugin: Navigated to ${url}`);\n  }\n\n  async onBrowserClose(browser: Browser): Promise<void> {\n    // Cleanup when browser closes\n    this.cdpService?.logger.info('Custom plugin: Browser closed');\n  }\n\n  async onShutdown(): Promise<void> {\n    // Cleanup when service shuts down\n    this.cdpService?.logger.info('Custom plugin: Service shutting down');\n  }\n}\n\n// Register the plugin\nfastify.cdpService.registerPlugin(new MyCustomPlugin({}));\n```\n\n### Available Plugin Hooks\n\nThe `BasePlugin` class provides these lifecycle hooks:\n\n- `onBrowserLaunch(browser)`: Called when browser instance starts\n- `onPageCreated(page)`: Called when a new page is created\n- `onPageNavigate(page)`: Called when a page navigates to a new URL\n- `onPageUnload(page)`: Called when a page is about to unload\n- `onBeforePageClose(page)`: Called before a page is closed\n- `onBrowserClose(browser)`: Called when browser instance closes\n- `onShutdown()`: Called during service shutdown\n- `onSessionEnd(sessionConfig)`: Called when a session ends\n\n## 🛠️ Development Workflow\n\n### Branch Naming Convention\n\n- `feature/description` - New features\n- `fix/description` - Bug fixes  \n- `docs/description` - Documentation updates\n- `refactor/description` - Code refactoring\n- `test/description` - Test additions/updates\n\n### Commit Message Format\n\nWe use [Conventional Commits](https://conventionalcommits.org/):\n\n```\ntype(scope): description\n\n[optional body]\n\n[optional footer]\n```\n\n**Types:**\n- `feat`: New feature\n- `fix`: Bug fix\n- `docs`: Documentation changes\n- `style`: Code style changes (formatting, etc.)\n- `refactor`: Code refactoring\n- `test`: Adding or updating tests\n- `chore`: Maintenance tasks\n\n**Examples:**\n```\nfeat(api): add session timeout configuration\nfix(ui): resolve session list refresh issue  \ndocs: update plugin development guide\ntest(api): add CDP service unit tests\n```\n\n### Code Style & Formatting\n\nWe use automated formatting and linting:\n\n```bash\n# Format code (API)\nnpm run pretty -w api\n\n# Lint code (UI)  \nnpm run lint -w ui\n\n# These run automatically on commit via Husky\n```\n\n**Style Guidelines:**\n- Use TypeScript for all new code\n- Follow existing patterns and conventions\n- Add JSDoc comments for public APIs\n- Use descriptive variable and function names\n- Keep functions small and focused\n\n### Testing\n\n> **Note**: We're currently building out our test suite! This is a great area for contributions.\n\n```bash\n# Tests are currently being set up - for now run these checks:\nnpm run build  # Type checking for both API and UI\nnpm run lint -w ui  # UI linting  \nnpm run pretty -w api  # API code formatting\n\n# When tests become available:\n# npm test -w api\n# npm test -w ui\n```\n\n**Testing Guidelines:**\n- Write unit tests for new functions and classes\n- Add integration tests for API endpoints\n- Include end-to-end tests for critical user flows\n- Mock external dependencies appropriately\n- Aim for meaningful test coverage, not just high percentages\n\n## 🔄 Pull Request Process\n\n### Before Submitting\n\n1. **Create an Issue** (for non-trivial changes)\n   - Describe the problem or feature request\n   - Discuss the approach with maintainers\n   - Reference the issue in your PR\n\n2. **Test Your Changes**\n   ```bash\n   # Build and test locally\n   npm run build\n   # npm test  # tests coming soon\n   \n   # Test with Docker\n   docker-compose -f docker-compose.dev.yml up --build\n   ```\n\n3. **Update Documentation**\n   - Update relevant README sections\n   - Add/update JSDoc comments\n   - Update API documentation if needed\n\n### PR Checklist\n\n- [ ] Branch is up-to-date with main\n- [ ] Code follows project style guidelines\n- [ ] Tests pass (when available)\n- [ ] Documentation is updated\n- [ ] Commit messages follow conventional format\n- [ ] PR description clearly explains changes\n- [ ] Breaking changes are documented\n\n### PR Template\n\n```markdown\n## Description\nBrief description of changes\n\n## Type of Change\n- [ ] Bug fix\n- [ ] New feature  \n- [ ] Breaking change\n- [ ] Documentation update\n\n## Testing\n- [ ] Tested locally\n- [ ] Added/updated tests\n- [ ] Tested with Docker\n\n## Related Issues\nFixes #(issue number)\n```\n\n## 🐛 Reporting Issues\n\n### Bug Reports\n\nUse our [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) and include:\n\n- Clear description of the issue\n- Steps to reproduce\n- Expected vs actual behavior\n- Environment details (OS, Node version, etc.)\n- Screenshots/logs if applicable\n\n### Feature Requests\n\n- Check existing issues first\n- Describe the use case and motivation\n- Provide examples of how it would work\n- Consider implementation complexity\n\n## 🌟 Good First Issues\n\nLooking for ways to contribute? Check out issues labeled:\n\n- `good first issue` - Perfect for newcomers\n- `help wanted` - We'd love community help\n- `documentation` - Improve our docs\n- `testing` - Help build our test suite\n\n## 📚 Resources\n\n### Learning Resources\n\n- [Puppeteer Documentation](https://pptr.dev/)\n- [Fastify Documentation](https://www.fastify.io/)\n- [React Documentation](https://react.dev/)\n- [TypeScript Handbook](https://www.typescriptlang.org/docs/)\n\n### Project Resources\n\n- [API Documentation](http://localhost:3000/documentation)\n- [Steel Cookbook](https://github.com/steel-dev/steel-cookbook) - Usage examples\n- [Discord Community](https://discord.gg/steel-dev) - Get help and discuss\n\n## 🤝 Community Guidelines\n\n### Code of Conduct\n\nWe are committed to providing a welcoming and inclusive environment for all contributors. Please:\n\n- Be respectful and constructive in discussions\n- Help newcomers and answer questions\n- Provide helpful feedback in code reviews\n- Report any unacceptable behavior to maintainers\n\n### Getting Help\n\n- **Discord**: Join our [Discord server](https://discord.gg/steel-dev) for real-time help\n- **GitHub Issues**: For bug reports and feature requests\n- **GitHub Discussions**: For questions and general discussion\n\n### Recognition\n\nWe appreciate all contributions! Contributors are recognized:\n\n- In our README contributors section\n- In our Discord server + Changelog announcements\n- Through GitHub's contribution tracking\n- In release notes for significant contributions\n- Potential invitation to join the core team\n\n## 🔧 Advanced Development\n\n### Environment Variables\n\nKey environment variables for development:\n\n```bash\n# API Configuration\nNODE_ENV=development\nHOST=0.0.0.0\nPORT=3000\nCHROME_HEADLESS=false  # For debugging\nENABLE_CDP_LOGGING=true  # For detailed logs\n\n# UI Configuration  \nAPI_URL=http://localhost:3000\n```\n\n### Debugging\n\n```bash\n# Debug API with Chrome DevTools\nnode --inspect ./api/build/index.js\n\n# Debug with VS Code\n# Use the provided launch configurations\n\n# Enable verbose logging\nENABLE_VERBOSE_LOGGING=true npm run dev -w api\n```\n\n## 📝 Documentation\n\n### Writing Documentation\n\n- Use clear, concise language\n- Include code examples\n- Add screenshots for UI features\n- Keep examples up-to-date\n- Follow markdown best practices\n\n### Documentation Structure\n\n- **README.md**: Project overview and quick start\n- **CONTRIBUTING.md**: This file - contribution guidelines\n- **API docs**: Auto-generated from OpenAPI schemas\n- **Architecture docs**: High-level system design\n- **Plugin docs**: Plugin development guides\n\n## 🚀 Release Process\n\n### Automated Releases\n\nWe use automated semantic versioning based on conventional commits:\n\n- **Automatic Version Bumping**: Versions are automatically bumped based on commit messages\n  - `patch`: Commits with `patch`, `fix`, `fixes`, or `docs` \n  - `minor`: Commits with `feat`, `feature`, or `minor`\n  - `major`: Commits with `breaking`, `breaking-change`, or `major`\n\n- **Beta Releases**: All releases are tagged with `-beta` suffix initially\n- **Automatic Changelog**: Generated from commit history with categorized changes\n- **GitHub Releases**: Automatically created with changelog and community links\n\n### Versioning\n\nWe follow [Semantic Versioning](https://semver.org/):\n\n- **MAJOR**: Breaking changes (triggered by `breaking`, `breaking-change`, `major` in commits)\n- **MINOR**: New features, backwards compatible (triggered by `feat`, `feature`, `minor`)\n- **PATCH**: Bug fixes, backwards compatible (triggered by `patch`, `fix`, `fixes`, `docs`)\n\n### Release Workflow\n\nThe release process is fully automated via GitHub Actions:\n\n1. **Push to main**: Any push to the main branch triggers the release workflow\n2. **Version bump**: Automatically determines version based on commit messages\n3. **Changelog generation**: Creates categorized changelog from commits\n4. **GitHub release**: Creates release with changelog and community links\n5. **Tag creation**: Tags the release with the new version\n\n### Manual Release Steps (if needed)\n\nIf manual intervention is required:\n\n1. Ensure commit messages follow conventional format\n2. Push changes to main branch\n3. Monitor the GitHub Actions workflow\n4. Verify the release was created successfully\n5. Announce on Discord/social media\n\n---\n\n## Thank You! 🙏\n\nThank you for contributing to Steel Browser! Your contributions help make browser automation more accessible and powerful for developers worldwide.\n\n**Happy hacking!** 🎉 "
  },
  {
    "path": "Dockerfile",
    "content": "ARG NODE_VERSION=22.13.0\n\nFROM node:${NODE_VERSION} AS base\n\nWORKDIR /app\n\nENV NODE_ENV=\"production\" \\\n    PUPPETEER_CACHE_DIR=/app/.cache \\\n    DISPLAY=:10 \\\n    PATH=\"/usr/bin:/app/selenium/driver:${PATH}\" \\\n    CHROME_BIN=/usr/bin/chromium \\\n    CHROME_PATH=/usr/bin/chromium\n\nLABEL org.opencontainers.image.source=\"https://github.com/steel-dev/steel-browser\"\n\n# Install dependencies\nRUN rm -f /etc/apt/apt.conf.d/docker-clean; \\\n    echo 'Binary::apt::APT::Keep-Downloaded-Packages \"true\";' > /etc/apt/apt.conf.d/keep-cache; \\\n    apt-get update -qq && \\\n    DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade\n\n# Stage 1: Build UI\nFROM node:${NODE_VERSION} AS ui-build\n\nWORKDIR /app\n\n# Copy root workspace files for UI build\nCOPY --link package.json package-lock.json ./\nCOPY --link ui/ ./ui/\n\n# Install UI dependencies and build with correct base path\nRUN npm ci --include=dev -w ui --ignore-scripts\nRUN VITE_API_URL=\"\" VITE_WS_URL=\"\" npm run build -w ui -- --base=/ui\n\n# Stage 2: Build API\nFROM base AS api-build\n\nRUN apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -y \\\n    build-essential \\\n    pkg-config \\\n    python-is-python3 \\\n    xvfb\n\n# Copy root workspace files for API build\nCOPY --link package.json package-lock.json ./\n\n# Remove or override the prepare script to avoid husky in Docker\nRUN npm pkg set scripts.prepare=\"echo skip husky\"\n\nCOPY --link api/ ./api/\n\n# Install dependencies for API\nRUN npm ci --include=dev --workspace=api\n\n# Install dependencies for recorder extension separately\nRUN cd api/extensions/recorder && npm ci --include=dev && cd -\n\n# Build the API package\nRUN npm run build -w api\n\n# Build the recorder extension\nRUN cd api/extensions/recorder && \\\n    npm run build && \\\n    cd -\n\n# Prune dev dependencies\nRUN npm prune --omit=dev -w api\nRUN cd api/extensions/recorder && npm prune --omit=dev && cd -\n\n# Stage 3: Production\nFROM base AS production\n\n# Install production dependencies\nRUN apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \\\n    wget \\\n    nginx \\\n    gnupg \\\n    fonts-ipafont-gothic \\\n    fonts-wqy-zenhei \\\n    fonts-thai-tlwg \\\n    fonts-kacst \\\n    fonts-freefont-ttf \\\n    libxss1 \\\n    xvfb \\\n    curl \\\n    unzip \\\n    default-jre \\\n    dbus \\\n    dbus-x11 \\\n    procps \\\n    x11-xserver-utils\n\n# Install Chrome and ChromeDriver\nRUN apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\\n    wget \\\n    ca-certificates \\\n    curl \\\n    unzip \\\n    # Download and install Chromium\n    && apt-get install -y chromium chromium-driver \\\n    # Clean up\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* \\\n    && rm -rf /var/cache/apt/*\n\nRUN mkdir -p /files\n\n# Copy the built API from api-build stage\nCOPY --from=api-build /app /app\n\n# Copy the built UI from ui-build stage into the API container\nCOPY --from=ui-build /app/ui/dist /app/ui/dist\n\n# Copy entrypoint script\nCOPY --chmod=755 api/entrypoint.sh /app/api/entrypoint.sh\n\nEXPOSE 3000 9223\n\nENV HOST_IP=localhost \\\n    DBUS_SESSION_BUS_ADDRESS=autolaunch:\n\nENTRYPOINT [\"/app/api/entrypoint.sh\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<br />\n<p align=\"center\">\n<a href=\"https://steel.dev\">\n  <img src=\"images/steel_header_logo.png\" alt=\"Steel Logo\" width=\"100\">\n</a>\n</p>\n\n\n\n<h3 align=\"center\"><b>Steel</b></h3>\n<p align=\"center\">\n    <b>The open-source browser API for AI agents & apps.</b> <br />\n    The best way to build live web agents and browser automation tools.\n</p>\n\n<div align=\"center\">\n\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/steel-dev/steel-browser?color=yellow)](https://github.com/steel-dev/steel-browser/commits/main)\n[![License](https://img.shields.io/github/license/steel-dev/steel-browser?color=yellow)](https://github.com/steel-dev/steel-browser/blob/main/LICENSE)\n[![Discord](https://img.shields.io/discord/1285696350117167226?label=discord)](https://discord.gg/steel-dev)\n[![Twitter Follow](https://img.shields.io/twitter/follow/steeldotdev)](https://twitter.com/steeldotdev)\n[![GitHub stars](https://img.shields.io/github/stars/steel-dev/steel-browser)](https://github.com/steel-dev/steel-browser)\n\n</div>\n\n<h4 align=\"center\">\n    <a href=\"https://app.steel.dev/sign-up\" target=\"_blank\">\n      Get Started\n  </a>  ·\n    <a href=\"https://docs.steel.dev/\" target=\"_blank\">\n      Documentation\n  </a>  ·\n  <a href=\"https://steel.dev/\" target=\"_blank\">\n      Website\n  </a> ·\n  <a href=\"https://github.com/steel-dev/steel-cookbook\" target=\"_blank\">\n      Cookbook\n  </a>\n</h4>\n\n<p align=\"center\">\n  <img src=\"images/demo.gif\" alt=\"Steel Demo\" width=\"600\">\n</p>\n\n## ✨ Highlights\n\n[Steel.dev](https://steel.dev) is an open-source browser API that makes it easy to build AI apps and agents that interact with the web. Instead of building automation infrastructure from scratch, you can focus on your AI application while Steel handles the complexity.\n\nUnder the hood, it manages sessions, pages, and browser processes, allowing you to perform complex browsing tasks programmatically without any of the headaches:\n- **Full Browser Control**: Uses Puppeteer and CDP for complete control over Chrome instances -- allowing you to connect using Puppeteer, Playwright, or Selenium.\n- **Session Management**: Maintains browser state, cookies, and local storage across requests\n- **Proxy Support**: Built-in proxy chain management for IP rotation\n- **Extension Support**: Load custom Chrome extensions for enhanced functionality\n- **Debugging Tools**: Built-in request logging and a UI to view/debug sessions with\n- **Anti-Detection**: Includes stealth plugins and fingerprint management\n- **Resource Management**: Automatic cleanup and browser lifecycle management\n- **Browser Tools**: Exposes APIs to quick convert pages to markdown, readability, screenshots, or PDFs.\n\n\nFor detailed API documentation and examples, check out our [API reference](https://docs.steel.dev/api-reference) or explore the Swagger UI directly at `http://0.0.0.0:3000/documentation`.\n\n> Steel is in public beta and evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.gg/steel-dev) or raise a GitHub issue. We read everything, respond to most, and love you.\n\nIf you love open-source, AI, and dev tools, [we're hiring across the stack](https://jobs.ashbyhq.com/steel)!\n\n### Make sure to give us a star ⭐\n\n<img width=\"200\" alt=\"Start us on Github!\" src=\"images/star_img.png\">\n\n## 🛠️ Getting Started\nThe easiest way to get started with Steel is by creating a [Steel Cloud](https://app.steel.dev) account. Otherwise, you can deploy this Steel browser instance to a cloud provider or run it locally.\n\n## ⚡ Quick Deploy\nIf you're looking to deploy to a cloud provider, we've got you covered.\n\n| Deployment methods | Link |\n| -------------------- | ----- |\n| Pre-built Docker Image (combined API + UI) | [![Deploy with Github Container Registry](https://img.shields.io/badge/GHCR-478CFF?style=for-the-badge&labelColor=478CFF&logo=github&logoColor=white)](https://github.com/steel-dev/steel-browser/pkgs/container/steel-browser) |\n| 1-click deploy to Railway | [![Deploy on Railway](https://img.shields.io/badge/Railway-B039CB?style=for-the-badge&labelColor=B039CB&logo=railway&logoColor=white)](https://railway.app/deploy/steelbrowser) |\n| 1-click deploy to Render | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy) |\n\n\n## 💻 Running Locally\n\n### Docker\n\nThe simplest way to deploy/run a Steel browser instance locally is to run the pre-built Docker image:\n\n```bash\n# Pull and run the Docker image\ndocker run -p 3000:3000 -p 9223:9223 ghcr.io/steel-dev/steel-browser\n```\n\nThis will start the Steel browser server on port 3000 (http://localhost:3000) and the UI at http://localhost:3000/ui. The 9223 port is used for the console debugger.\n\nYou can now create sessions, scrape pages, take screenshots, and more. Jump to the [Usage](#usage) section for some quick examples on how you can do that.\n\nAlternatively, you can run the API and UI separately with docker compose:\n\n```bash\ndocker compose up\n```\n\nFor Mac Silicon users, you will need to pass this env flag to the Docker compose command to run the images on the correct platform:\n```bash\nDOCKER_DEFAULT_PLATFORM=linux/arm64 docker compose up\n```\n\n## Quickstart for Contributors\nWhen developing locally, you will need to run the [`docker-compose.dev.yml`](./docker-compose.dev.yml) file instead of the default [`docker-compose.yml`](./docker-compose.yml) file so that your local changes are reflected. Doing this will build the Docker images from the [`api`](./api) and [`ui`](./ui) directories and run the server and UI on port 3000 and 5173 respectively.\n\n```bash\ndocker compose -f docker-compose.dev.yml up\n```\n\nYou will also need to run it with `--build` to ensure the Docker images are re-built every time you make changes:\n\n```bash\ndocker compose -f docker-compose.dev.yml up --build\n```\n\nIf you run on a custom host, create a `.env` file (see `docs/DEVELOPMENT_SETUP.md` for variables) or modify the environment variables used by `docker-compose.dev.yml` to use your host.\n\n### Node.js\nAlternatively, if you have Node.js and Chrome installed, you can run both the server and the UI directly:\n\n```bash\nnpm install\nnpm run dev\n```\n\nThis will also start the Steel server on port 3000 and the UI on port 5173.\n\nMake sure you have the Chrome executable installed and in one of these paths:\n\n- **Linux**:\n  `/usr/bin/google-chrome`\n\n- **MacOS**:\n  `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`\n\n- **Windows**:\n  - `C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe` OR\n  - `C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe`\n\n#### Custom Chrome Executable\n\nIf you have a custom Chrome executable or a different path, you can set the `CHROME_EXECUTABLE_PATH` environment variable to the path of your Chrome executable:\n\n```bash\nexport CHROME_EXECUTABLE_PATH=/path/to/your/chrome\nnpm run dev\n```\n\nFor more details on where this is checked look at [`api/src/utils/browser.ts`](./api/src/utils/browser.ts).\n\n## 🏄🏽‍♂️ Usage\n> If you're looking for quick examples on how to use Steel, check out the [Cookbook](https://github.com/steel-dev/steel-cookbook).\n>\n> Alternatively you can play with the [REPL package](./repl/README.md) too `cd repl` and `npm run start`\n\nThere are two main ways to interact with the Steel browser API:\n1. [Using Sessions](#sessions)\n2. [Using the Quick Actions Endpoints](#quick-actions-api)\n\nIn these examples, we assume your custom Steel API endpoint is `http://localhost:3000`.\n\nThe full REST OpenAPI documentation can be found [on our site](https://docs.steel.dev/api-reference) and on your local Steel instance at `http://localhost:3000/documentation`.\n\n#### Using the SDKs\nIf you prefer to use the our Python and Node SDKs, you can install the `steel-sdk` package for Node or Python.\n\nThese SDKs are built on top of the REST API and provide a more convenient way to interact with the Steel browser API. They are fully typed, and are compatible with both Steel Cloud and self-hosted Steel instances (changeable using the `baseURL` option on Node and `base_url` on Python).\n\nFor more details on installing and using the SDKs, please see the [Node SDK Reference](https://github.com/steel-dev/steel-node/blob/main/api.md) and the [Python SDK Reference](https://github.com/steel-dev/steel-python/blob/main/api.md).\n\n\n### Sessions\nThe `/sessions` endpoint lets you relaunch the browser with custom options or extensions (e.g. with a custom proxy) and also reset the browser state. Perfect for complex, stateful workflows that need fine-grained control.\n\nOnce you have a session, you can use the session ID or the root URL to interact with the browser. To do this, you will need to use Puppeteer or Playwright. You can find some examples of how to use Puppeteer and Playwright with Steel in the docs below:\n* [Puppeteer Integration](https://docs.steel.dev/overview/guides/puppeteer)\n* [Playwright with Node](https://docs.steel.dev/overview/guides/playwright-node)\n* [Playwright with Python](https://docs.steel.dev/overview/guides/playwright-python)\n\n<details open>\n<summary><b>Creating a Session using the Node SDK</b></summary>\n<br>\n\n```typescript\nimport Steel from 'steel-sdk';\n\nconst client = new Steel({\n  baseURL: \"http://localhost:3000\", // Custom API Base URL override\n});\n\n(async () => {\n  try {\n    // Create a new browser session with current API fields\n    const session = await client.sessions.create({\n      blockAds: true,\n      proxyUrl: \"user:pass@host:port\", // optional\n      dimensions: { width: 1280, height: 800 }, // optional\n    });\n    console.log(\"Created session with ID:\", session.id);\n  } catch (error) {\n    console.error(\"Error creating session:\", error);\n  }\n})();\n````\n</details>\n\n<details>\n<summary><b>Creating a Session using the Python SDK</b></summary>\n<br>\n\n````python\nimport os\nfrom steel import Steel\n\nclient = Steel(\n    base_url=\"http://localhost:3000\",  # Custom API Base URL override\n)\n\ntry:\n    # Create a new browser session with custom options\n    session = client.sessions.create(\n        block_ads=True,\n        proxy_url=\"user:pass@host:port\",  # optional\n        dimensions={\"width\": 1280, \"height\": 800},  # optional\n    )\n    print(\"Created session with ID:\", session.id)\nexcept Exception as e:\n    print(\"Error creating session:\", e)\n````\n</details>\n\n<details>\n<summary><b>Creating a Session using Curl</b></summary>\n<br>\n\n```bash\n# Launch a new browser session\ncurl -X POST http://localhost:3000/v1/sessions \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"proxyUrl\": \"user:pass@host:port\",\n    \"blockAds\": true,\n    \"dimensions\": { \"width\": 1280, \"height\": 800 }\n  }'\n```\n</details>\n\n\n#### Selenium Sessions\n>**Note:** This integration does not support all the features of the CDP-based browser sessions API.\n\nFor teams with existing Selenium workflows, the Steel browser provides a drop-in replacement that adds enhanced features while maintaining compatibility. You can simply use the `isSelenium` option to create a Selenium session:\n\n```typescript\n// Using the Node SDK\nconst session = await client.sessions.create({ isSelenium: true });\n```\n```python\n# Using the Python SDK\nsession = client.sessions.create(is_selenium=True)\n```\n<details>\n<summary><b>Using Curl</b></summary>\n<br>\n\n```bash\n# Launch a Selenium session\ncurl -X POST http://localhost:3000/v1/sessions \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"isSelenium\": true\n  }'\n```\n</details>\n<br>\n\nThe Selenium API is fully compatible with Selenium's WebDriver protocol, so you can use any existing Selenium clients to connect to the Steel browser. **For more details on using Selenium with Steel, refer to the [Selenium Docs](https://docs.steel.dev/overview/guides/selenium).**\n\n### Quick Actions API\nThe `/scrape`, `/screenshot`, and `/pdf` endpoints let you quickly extract clean, well-formatted data from any webpage using the running Steel server. Ideal for simple, read-only, on-demand jobs:\n\n<details open>\n<summary><b>Scrape a Web Page</b></summary>\n<br>\n\nExtract the HTML content of a web page.\n\n```bash\n# Example using the Actions API\ncurl -X POST http://0.0.0.0:3000/v1/scrape \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"delay\": 1000\n  }'\n```\n</details>\n\n<details>\n<summary><b>Take a Screenshot</b></summary>\n<br>\n\nTake a screenshot of a web page.\n```bash\n# Example using the Actions API\ncurl -X POST http://0.0.0.0:3000/v1/screenshot \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\",\n    \"fullPage\": true\n  }' --output screenshot.png\n```\n</details>\n\n<details>\n<summary><b>Download a PDF</b></summary>\n<br>\n\nDownload a PDF of a web page.\n```bash\n# Example using the Actions API\ncurl -X POST http://0.0.0.0:3000/v1/pdf \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com\"\n  }' --output output.pdf\n```\n</details>\n\n## Get involved\nSteel browser is an open-source project, and we welcome contributions!\n- Questions/ideas/feedback? Come hangout on [Discord](https://discord.gg/steel-dev)\n- Found a bug? Open an issue on [GitHub](https://github.com/steel-dev/steel-browser/issues)\n\n## License\n[Apache 2.0](./LICENSE)\n\n---\n\nMade with ❤️ by the Steel team.\n"
  },
  {
    "path": "api/.dockerignore",
    "content": "node_modules/\nbuild/\n**/.env\n**/.env.local\nDockerfile\ndocker-compose.yml\napi/extensions/**/dist\n"
  },
  {
    "path": "api/.env.example",
    "content": "# Server configuration\nNODE_ENV=development\nHOST=0.0.0.0\nPORT=3000\n# Use DOMAIN if you want to specify a full domain name instead of HOST:PORT\n# DOMAIN=example.com\n\n# Set to true to use HTTPS/WSS instead of HTTP/WS\nUSE_SSL=false\n\n# Chrome/CDP configuration\nCHROME_HEADLESS=true\nCHROME_EXECUTABLE_PATH=\nCHROME_ARGS=\nCDP_REDIRECT_PORT=9223\n# CDP_DOMAIN=example.com:9223\n\n# Optional proxy configuration\nPROXY_URL=\n\n# Logging\nLOG_LEVEL=warn # fatal, error, warn, info, debug, trace\nENABLE_CDP_LOGGING=false\nLOG_CUSTOM_EMIT_EVENTS=false\nENABLE_VERBOSE_LOGGING=false\n\n# Other configuration options\nSKIP_FINGERPRINT_INJECTION=false\nDEFAULT_TIMEZONE=\nDEFAULT_HEADERS=\n"
  },
  {
    "path": "api/.gitattributes",
    "content": "src/scripts/* linguist-vendored\nextensions/* linguist-vendored\n"
  },
  {
    "path": "api/.gitignore",
    "content": "node_modules\n.pnp\n.pnp.js\ncoverage\n.DS_Store\n*.pem\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n.env\n.env.local\n.env.development.local\n.test.env.local\n.env.production.local\n!.env.example\nproduction.env\n.turbo\nbuild\ndb/data\n*.tsbuildinfo\nout\n.idea\n*.env\ndist\n.aider*\nextensions/*\n!extensions/recorder\nfiles/*\nstatic/*\n# Ignore future changes to this file\nsrc/services/cdp-lifecycle.service.ts"
  },
  {
    "path": "api/.prettierignore",
    "content": "# Ignore artifacts:\nbuild\ncoverage\n"
  },
  {
    "path": "api/.prettierrc",
    "content": "{\n  \"printWidth\": 100,\n  \"tabWidth\": 2,\n  \"semi\": true,\n  \"singleQuote\": false,\n  \"bracketSpacing\": true,\n  \"arrowParens\": \"always\"\n}"
  },
  {
    "path": "api/.puppeteerrc.cjs",
    "content": "const { join } = require(\"path\");\n\n/**\n * @type {import(\"puppeteer\").Configuration}\n */\nmodule.exports = {\n  defaultProduct: \"chrome\",\n  cacheDirectory: join(__dirname, \".cache\", \"puppeteer\"),\n};\n"
  },
  {
    "path": "api/Dockerfile",
    "content": "ARG NODE_VERSION=22.13.0\n\nFROM node:${NODE_VERSION}-slim AS base\n\nWORKDIR /app\n\nENV NODE_ENV=\"production\" \\\n    PUPPETEER_CACHE_DIR=/app/.cache \\\n    DISPLAY=:10 \\\n    PATH=\"/usr/bin:/app/selenium/driver:${PATH}\" \\\n    CHROME_BIN=/usr/bin/chromium \\\n    CHROME_PATH=/usr/bin/chromium\n\nLABEL org.opencontainers.image.source=\"https://github.com/steel-dev/steel-browser\"\n\n# Install dependencies\nRUN rm -f /etc/apt/apt.conf.d/docker-clean; \\\n    echo 'Binary::apt::APT::Keep-Downloaded-Packages \"true\";' > /etc/apt/apt.conf.d/keep-cache; \\\n    apt-get update -qq && \\\n    DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \\\n    apt-get install -y --no-install-recommends \\\n    expat \\\n    libxslt1.1 \\\n    libpam0g \\\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/*\n\nFROM base AS build\n\nRUN apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -y \\\n    build-essential \\\n    pkg-config \\\n    python-is-python3 \\\n    xvfb\n\n# Copy root workspace files first\nCOPY --link package.json package-lock.json ./\n\n# Remove or override the prepare script to avoid husky in Docker\nRUN npm pkg set scripts.prepare=\"echo skip husky\"\n\nCOPY --link api/ ./api/\n\n# Install dependencies for api\nRUN npm ci --include=dev --workspace=api\n\n# Install dependencies for recorder extension separately\nRUN cd api/extensions/recorder && npm ci --include=dev && cd -\n\n# Build the api package\nRUN npm run build -w api\n\nRUN cd api/extensions/recorder && \\\n    npm run build && \\\n    cd -\n\n# Prune dev dependencies\nRUN npm prune --omit=dev -w api\nRUN cd api/extensions/recorder && npm prune --omit=dev && cd -\n\nFROM base AS production\n# Install dependencies\nRUN apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \\\n    wget \\\n    nginx \\\n    gnupg \\\n    fonts-ipafont-gothic \\\n    fonts-wqy-zenhei \\\n    fonts-thai-tlwg \\\n    fonts-kacst \\\n    fonts-freefont-ttf \\\n    libxss1 \\\n    xvfb \\\n    curl \\\n    unzip \\\n    default-jre \\\n    dbus \\\n    dbus-x11 \\\n    procps \\\n    x11-xserver-utils\n\n# Install Chrome and ChromeDriver\nRUN apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \\\n    wget \\\n    ca-certificates \\\n    curl \\\n    unzip \\\n    # Download and install Chromium\n    && apt-get install -y chromium chromium-driver \\\n    # Clean up\n    && apt-get clean \\\n    && rm -rf /var/lib/apt/lists/* \\\n    && rm -rf /var/cache/apt/*\n\nRUN mkdir -p /files\n\nCOPY --chmod=755 api/entrypoint.sh /app/api/entrypoint.sh\n\nEXPOSE 3000 9223\n\nENV HOST_IP=localhost \\\n    DBUS_SESSION_BUS_ADDRESS=autolaunch:\n\nENTRYPOINT [\"/app/api/entrypoint.sh\"]\n\nCOPY --from=build /app /app\n"
  },
  {
    "path": "api/entrypoint.sh",
    "content": "#!/bin/sh\nset -e  # Exit on error\n\n# Function to log with timestamp\nlog() {\n    if [ \"$DEBUG\" = \"true\" ]; then\n        echo \"[$(date '+%Y-%m-%d %H:%M:%S')] $1\"\n    fi\n}\n\n# Initialize DBus\ninit_dbus() {\n    log \"Initializing DBus...\"\n    mkdir -p /var/run/dbus\n\n    if [ -e /var/run/dbus/pid ]; then\n        rm -f /var/run/dbus/pid\n    fi\n\n    dbus-daemon --system --fork\n    sleep 2  # Give DBus time to initialize\n\n    if dbus-send --system --print-reply --dest=org.freedesktop.DBus \\\n        /org/freedesktop/DBus org.freedesktop.DBus.ListNames >/dev/null 2>&1; then\n        log \"DBus initialized successfully\"\n        return 0\n    else\n        log \"ERROR: DBus failed to initialize\"\n        return 1\n    fi\n}\n\n# Verify Chrome and ChromeDriver installation\nverify_chrome() {\n    log \"Verifying Chrome installation...\"\n\n    # Check Chrome binary and version\n    if [ ! -f \"/usr/bin/chromium\" ] && [ -z \"$CHROME_EXECUTABLE_PATH\" ]; then\n        log \"ERROR: Chrome binary not found at /usr/bin/chromium and CHROME_EXECUTABLE_PATH not set\"\n        return 1\n    fi\n\n    if [ -f \"/usr/bin/chromium\" ]; then\n        chrome_version=$(chromium --version 2>/dev/null || echo \"unknown\")\n    elif [ -n \"$CHROME_EXECUTABLE_PATH\" ] && [ -f \"$CHROME_EXECUTABLE_PATH\" ]; then\n        chrome_version=$(\"$CHROME_EXECUTABLE_PATH\" --version 2>/dev/null || echo \"unknown\")\n    else\n        chrome_version=\"unknown\"\n    fi\n    log \"Chrome version: $chrome_version\"\n\n    # Check ChromeDriver binary and version\n    if [ ! -f \"/usr/bin/chromedriver\" ]; then\n        log \"ERROR: ChromeDriver not found at /usr/bin/chromedriver\"\n        return 1\n    fi\n\n    chromedriver_version=$(chromedriver --version 2>/dev/null || echo \"unknown\")\n    log \"ChromeDriver version: $chromedriver_version\"\n\n    log \"Chrome environment configured successfully\"\n    return 0\n}\n\n# Start nginx with better error handling\nstart_nginx() {\n    if [ \"$START_NGINX\" = \"true\" ]; then\n        log \"Starting nginx...\"\n        nginx -c /app/api/nginx.conf\n        \n        # Wait for nginx to start\n        max_attempts=10\n        attempt=1\n        while [ $attempt -le $max_attempts ]; do\n            if nginx -t >/dev/null 2>&1; then\n                log \"Nginx started successfully\"\n                return 0\n            fi\n            log \"Attempt $attempt/$max_attempts: Waiting for nginx...\"\n            attempt=$((attempt + 1))\n            sleep 1\n        done\n        log \"ERROR: Nginx failed to start properly\"\n        return 1\n    else\n        log \"Skipping nginx startup (--no-nginx flag detected)\"\n        return 0\n    fi\n}\n\n# Main execution\nmain() {\n    # Parse arguments\n    START_NGINX=true\n    for arg in \"$@\"; do\n        if [ \"$arg\" = \"--no-nginx\" ]; then\n            START_NGINX=false\n            break\n        fi\n    done\n    \n    if [ \"$DEBUG\" = \"true\" ]; then\n        init_dbus || exit 1\n        verify_chrome || exit 1\n    fi\n    start_nginx || exit 1\n    \n    # Set required environment variables\n    export CDP_REDIRECT_PORT=9223\n    export DISPLAY=:10\n    \n    # Log environment state\n    log \"Environment configuration:\"\n    log \"HOST=$HOST\"\n    log \"CDP_REDIRECT_PORT=$CDP_REDIRECT_PORT\"\n    log \"NODE_ENV=$NODE_ENV\"\n    \n    # Start the application\n    # Run the `npm run start` command but without npm.\n    # NPM will introduce its own signal handling\n    # which will prevent the container from waiting\n    # for a session to be released before stopping gracefully\n    log \"Starting Steel Browser API...\"\n    exec node ./api/build/index.js\n}\n\nmain \"$@\""
  },
  {
    "path": "api/extensions/recorder/.gitignore",
    "content": "node_modules/\ndist/\n"
  },
  {
    "path": "api/extensions/recorder/manifest.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"Steel Recording Extension\",\n  \"version\": \"1.0\",\n  \"permissions\": [\n    \"scripting\",\n    \"activeTab\"\n  ],\n  \"host_permissions\": [\n    \"http://localhost:3000/*\",\n    \"http://0.0.0.0:3000/*\"\n  ],\n  \"background\": {\n    \"service_worker\": \"dist/background.js\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\n        \"<all_urls>\"\n      ],\n      \"js\": [\n        \"dist/inject.js\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "api/extensions/recorder/package.json",
    "content": "{\n  \"name\": \"steel-recording-extension\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"rrweb\": \"^2.0.0-alpha.4\",\n    \"@rrweb/packer\": \"^2.0.0-alpha.4\"\n  },\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"devDependencies\": {\n    \"webpack\": \"^5.89.0\",\n    \"webpack-cli\": \"^5.1.4\"\n  }\n}"
  },
  {
    "path": "api/extensions/recorder/src/background.js",
    "content": "const LOCAL_API_URL = \"http://localhost:3000/v1/events\";\nconst FALLBACK_API_URL = \"http://0.0.0.0:3000/v1/events\"; // Need to point to 0.0.0.0 in some deploys\nlet currentApiUrl = LOCAL_API_URL;\n\nasync function injectScript(tabId, changeInfo, tab) {\n  if (changeInfo.status === \"complete\" && tab.url) {\n    try {\n      await chrome.scripting.executeScript({\n        target: { tabId },\n        files: [\"inject.js\"],\n      });\n    } catch (error) {\n      console.error(\"Script injection failed:\", error);\n    }\n  }\n}\n\n// Listen for tab updates\nchrome.tabs.onUpdated.addListener(injectScript);\n\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n  if (message.type !== \"SAVE_EVENTS\") {\n    return false;\n  }\n\n  console.log(\"[Recorder Background] Saving events to\", currentApiUrl);\n\n  const sendEvents = async (url) => {\n    try {\n      const response = await fetch(url, {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify(message),\n      });\n\n      if (!response.ok) {\n        throw new Error(`HTTP error! status: ${response.status}`);\n      }\n\n      sendResponse({ success: true });\n    } catch (error) {\n      if (url === LOCAL_API_URL) {\n        // Retry with fallback URL\n        currentApiUrl = FALLBACK_API_URL;\n        return sendEvents(FALLBACK_API_URL);\n      }\n      sendResponse({ success: false, error: error.message });\n    }\n  };\n\n  sendEvents(currentApiUrl);\n  return true;\n});\n"
  },
  {
    "path": "api/extensions/recorder/src/inject.js",
    "content": "import { record } from \"rrweb\";\nimport { pack } from \"@rrweb/packer\";\n\nrecord({\n  emit: (event) => {\n    chrome.runtime.sendMessage(\n      {\n        type: \"SAVE_EVENTS\",\n        events: [event],\n      },\n      (response) => {\n        if (!response.success) {\n          console.error(\"[Recorder] Failed to save events:\", response.error);\n        }\n      },\n    );\n  },\n  packFn: pack,\n  sampling: {\n    media: 800,\n  },\n  inlineImages: true,\n  collectFonts: true,\n  recordCrossOriginIframes: true,\n  recordCanvas: true,\n});\n\nconst enableWebRtcSites = [\"meet.google.com\", \"zoom.us\", \"discord.com\"];\n\ntry {\n  const hostname = new URL(window.location.href).hostname;\n  const shouldDisableWebRtc = !enableWebRtcSites.includes(hostname);\n\n  if (shouldDisableWebRtc) {\n    navigator.mediaDevices.getUserMedia =\n      navigator.webkitGetUserMedia =\n      navigator.mozGetUserMedia =\n      navigator.getUserMedia =\n      webkitRTCPeerConnection =\n      RTCPeerConnection =\n      MediaStreamTrack =\n        undefined;\n\n    Object.defineProperty(window, \"RTCPeerConnection\", {\n      get: () => {\n        return {};\n      },\n    });\n    Object.defineProperty(window, \"RTCDataChannel\", {\n      get: () => {\n        return {};\n      },\n    });\n  }\n} catch (e) {\n  console.error(`Error processing URL for WebRTC blocking: ${e}`);\n}\n"
  },
  {
    "path": "api/extensions/recorder/webpack.config.mjs",
    "content": "import path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { dirname } from \"path\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nexport default {\n  mode: \"production\",\n  entry: {\n    inject: path.resolve(__dirname, \"src/inject.js\"),\n    background: path.resolve(__dirname, \"src/background.js\"),\n  },\n  output: {\n    filename: \"[name].js\",\n    path: path.resolve(__dirname, \"dist\"),\n  },\n  optimization: {\n    minimize: false,\n  },\n};\n"
  },
  {
    "path": "api/nginx.conf",
    "content": "events {\n    worker_connections 1024;\n}\n\nhttp {\n    map $http_upgrade $connection_upgrade {\n        default upgrade;\n        ''      keep-alive;\n    }\n    server {\n        listen 9223;\n        \n        location / {\n            proxy_pass http://127.0.0.1:9222;\n            proxy_http_version 1.1;\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection $connection_upgrade;\n            proxy_set_header Host $host;\n            gzip off;\n            proxy_set_header Accept-Encoding \"\";\n            proxy_read_timeout 86400;\n            proxy_send_timeout 86400;\n            proxy_buffering off;\n            proxy_request_buffering off;\n            chunked_transfer_encoding on;\n        }\n    }\n} "
  },
  {
    "path": "api/openapi/generate.ts",
    "content": "import { writeFileSync } from \"fs\";\nimport { server } from \"../src\";\nimport { env } from \"../src/env.js\";\n\ninterface OpenAPIServer {\n  url: string;\n  description?: string;\n}\n\ninterface OpenAPIDocument {\n  servers?: OpenAPIServer[];\n  [key: string]: any;\n}\n\nserver.ready(() => {\n  let openApiJSON = server.swagger() as OpenAPIDocument;\n\n  // Add server URL from environment variables.\n  const serverUrl = `http://${env.HOST}:${env.PORT}`;\n  if (!openApiJSON.servers) {\n    openApiJSON.servers = [];\n  }\n  openApiJSON.servers.push({\n    url: serverUrl,\n    description: \"Local server from env variables\",\n  });\n\n  writeFileSync(\"./openapi/schemas.json\", JSON.stringify(openApiJSON, null, 2), \"utf-8\");\n  console.log(\"OpenAPI JSON has been written to schemas.json\");\n\n  server.close(() => {\n    console.log(\"Server closed after generating schemas.\");\n    process.exit(0);\n  });\n});\n"
  },
  {
    "path": "api/openapi/schemas.json",
    "content": "{\n  \"openapi\": \"3.0.3\",\n  \"info\": {\n    \"title\": \"Steel Browser Instance API\",\n    \"description\": \"Documentation for controlling a single instance of Steel Browser\",\n    \"version\": \"0.0.1\"\n  },\n  \"components\": {\n    \"securitySchemes\": {},\n    \"schemas\": {\n      \"ScrapeRequest\": {\n        \"title\": \"ScrapeRequest\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"url\": {\n            \"type\": \"string\"\n          },\n          \"format\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"html\",\n                \"readability\",\n                \"cleaned_html\",\n                \"markdown\"\n              ]\n            }\n          },\n          \"screenshot\": {\n            \"type\": \"boolean\"\n          },\n          \"pdf\": {\n            \"type\": \"boolean\"\n          },\n          \"proxyUrl\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"description\": \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\"\n          },\n          \"delay\": {\n            \"type\": \"number\"\n          },\n          \"logUrl\": {\n            \"type\": \"string\"\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"ScrapeResponse\": {\n        \"title\": \"ScrapeResponse\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {}\n          },\n          \"metadata\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"title\": {\n                \"type\": \"string\"\n              },\n              \"language\": {\n                \"type\": \"string\"\n              },\n              \"urlSource\": {\n                \"type\": \"string\"\n              },\n              \"timestamp\": {\n                \"type\": \"string\",\n                \"format\": \"date-time\"\n              },\n              \"description\": {\n                \"type\": \"string\"\n              },\n              \"keywords\": {\n                \"type\": \"string\"\n              },\n              \"author\": {\n                \"type\": \"string\"\n              },\n              \"ogTitle\": {\n                \"type\": \"string\"\n              },\n              \"ogDescription\": {\n                \"type\": \"string\"\n              },\n              \"ogImage\": {\n                \"type\": \"string\"\n              },\n              \"ogUrl\": {\n                \"type\": \"string\"\n              },\n              \"ogSiteName\": {\n                \"type\": \"string\"\n              },\n              \"articleAuthor\": {\n                \"type\": \"string\"\n              },\n              \"publishedTime\": {\n                \"type\": \"string\"\n              },\n              \"modifiedTime\": {\n                \"type\": \"string\"\n              },\n              \"canonical\": {\n                \"type\": \"string\"\n              },\n              \"favicon\": {\n                \"type\": \"string\"\n              },\n              \"jsonLd\": {},\n              \"statusCode\": {\n                \"type\": \"integer\"\n              }\n            },\n            \"required\": [\n              \"statusCode\"\n            ],\n            \"additionalProperties\": false\n          },\n          \"links\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"url\": {\n                  \"type\": \"string\"\n                },\n                \"text\": {\n                  \"type\": \"string\"\n                }\n              },\n              \"required\": [\n                \"url\",\n                \"text\"\n              ],\n              \"additionalProperties\": false\n            }\n          },\n          \"screenshot\": {\n            \"type\": \"string\"\n          },\n          \"pdf\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"content\",\n          \"metadata\",\n          \"links\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"ScreenshotRequest\": {\n        \"title\": \"ScreenshotRequest\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"url\": {\n            \"type\": \"string\"\n          },\n          \"proxyUrl\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"description\": \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\"\n          },\n          \"delay\": {\n            \"type\": \"number\"\n          },\n          \"fullPage\": {\n            \"type\": \"boolean\"\n          },\n          \"logUrl\": {\n            \"type\": \"string\"\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"ScreenshotResponse\": {\n        \"title\": \"ScreenshotResponse\"\n      },\n      \"PDFRequest\": {\n        \"title\": \"PDFRequest\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"url\": {\n            \"type\": \"string\"\n          },\n          \"proxyUrl\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"description\": \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\"\n          },\n          \"delay\": {\n            \"type\": \"number\"\n          },\n          \"logUrl\": {\n            \"type\": \"string\"\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"PDFResponse\": {\n        \"title\": \"PDFResponse\"\n      },\n      \"CreateSession\": {\n        \"title\": \"CreateSession\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"sessionId\": {\n            \"type\": \"string\",\n            \"format\": \"uuid\",\n            \"description\": \"Unique identifier for the session\"\n          },\n          \"proxyUrl\": {\n            \"type\": \"string\",\n            \"description\": \"Proxy URL to use for the session\"\n          },\n          \"userAgent\": {\n            \"type\": \"string\",\n            \"description\": \"User agent string to use for the session\"\n          },\n          \"sessionContext\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"cookies\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"name\": {\n                      \"type\": \"string\",\n                      \"description\": \"The name of the cookie\"\n                    },\n                    \"value\": {\n                      \"type\": \"string\",\n                      \"description\": \"The value of the cookie\"\n                    },\n                    \"url\": {\n                      \"type\": \"string\",\n                      \"description\": \"The URL of the cookie\"\n                    },\n                    \"domain\": {\n                      \"type\": \"string\",\n                      \"description\": \"The domain of the cookie\"\n                    },\n                    \"path\": {\n                      \"type\": \"string\",\n                      \"description\": \"The path of the cookie\"\n                    },\n                    \"secure\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether the cookie is secure\"\n                    },\n                    \"httpOnly\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether the cookie is HTTP only\"\n                    },\n                    \"sameSite\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"Strict\",\n                        \"Lax\",\n                        \"None\"\n                      ],\n                      \"description\": \"The same site attribute of the cookie\"\n                    },\n                    \"size\": {\n                      \"type\": \"number\",\n                      \"description\": \"The size of the cookie\"\n                    },\n                    \"expires\": {\n                      \"type\": \"number\",\n                      \"description\": \"The expiration date of the cookie\"\n                    },\n                    \"partitionKey\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"topLevelSite\": {\n                          \"type\": \"string\",\n                          \"description\": \"The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie.\"\n                        },\n                        \"hasCrossSiteAncestor\": {\n                          \"type\": \"boolean\",\n                          \"description\": \"Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.\"\n                        }\n                      },\n                      \"required\": [\n                        \"topLevelSite\",\n                        \"hasCrossSiteAncestor\"\n                      ],\n                      \"additionalProperties\": false,\n                      \"description\": \"The partition key of the cookie\"\n                    },\n                    \"session\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether the cookie is a session cookie\"\n                    },\n                    \"priority\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"Low\",\n                        \"Medium\",\n                        \"High\"\n                      ],\n                      \"description\": \"The priority of the cookie\"\n                    },\n                    \"sameParty\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether the cookie is a same party cookie\"\n                    },\n                    \"sourceScheme\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"Unset\",\n                        \"NonSecure\",\n                        \"Secure\"\n                      ],\n                      \"description\": \"The source scheme of the cookie\"\n                    },\n                    \"sourcePort\": {\n                      \"type\": \"number\",\n                      \"description\": \"The source port of the cookie\"\n                    }\n                  },\n                  \"required\": [\n                    \"name\",\n                    \"value\"\n                  ],\n                  \"additionalProperties\": false\n                },\n                \"description\": \"Cookies to initialize in the session\"\n              },\n              \"localStorage\": {\n                \"type\": \"object\",\n                \"additionalProperties\": {\n                  \"type\": \"object\",\n                  \"additionalProperties\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"description\": \"Domain-specific localStorage items to initialize in the session\"\n              },\n              \"sessionStorage\": {\n                \"type\": \"object\",\n                \"additionalProperties\": {\n                  \"type\": \"object\",\n                  \"additionalProperties\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"description\": \"Domain-specific sessionStorage items to initialize in the session\"\n              },\n              \"indexedDB\": {\n                \"type\": \"object\",\n                \"additionalProperties\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"number\"\n                      },\n                      \"name\": {\n                        \"type\": \"string\"\n                      },\n                      \"data\": {\n                        \"type\": \"array\",\n                        \"items\": {\n                          \"type\": \"object\",\n                          \"properties\": {\n                            \"id\": {\n                              \"type\": \"number\"\n                            },\n                            \"name\": {\n                              \"type\": \"string\"\n                            },\n                            \"records\": {\n                              \"type\": \"array\",\n                              \"items\": {\n                                \"type\": \"object\",\n                                \"properties\": {\n                                  \"key\": {},\n                                  \"value\": {},\n                                  \"blobFiles\": {\n                                    \"type\": \"array\",\n                                    \"items\": {\n                                      \"type\": \"object\",\n                                      \"properties\": {\n                                        \"blobNumber\": {\n                                          \"type\": \"number\"\n                                        },\n                                        \"mimeType\": {\n                                          \"type\": \"string\"\n                                        },\n                                        \"size\": {\n                                          \"type\": \"number\"\n                                        },\n                                        \"filename\": {\n                                          \"type\": \"string\"\n                                        },\n                                        \"lastModified\": {\n                                          \"type\": \"string\",\n                                          \"format\": \"date-time\"\n                                        },\n                                        \"path\": {\n                                          \"type\": \"string\"\n                                        }\n                                      },\n                                      \"required\": [\n                                        \"blobNumber\",\n                                        \"mimeType\",\n                                        \"size\"\n                                      ],\n                                      \"additionalProperties\": false\n                                    }\n                                  }\n                                },\n                                \"additionalProperties\": false\n                              }\n                            }\n                          },\n                          \"required\": [\n                            \"id\",\n                            \"name\",\n                            \"records\"\n                          ],\n                          \"additionalProperties\": false\n                        }\n                      }\n                    },\n                    \"required\": [\n                      \"id\",\n                      \"name\",\n                      \"data\"\n                    ],\n                    \"additionalProperties\": false\n                  }\n                },\n                \"description\": \"Domain-specific indexedDB items to initialize in the session\"\n              }\n            },\n            \"additionalProperties\": false,\n            \"description\": \"Session context data to be used in the created session\"\n          },\n          \"isSelenium\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates if Selenium is used in the session\"\n          },\n          \"blockAds\": {\n            \"type\": \"boolean\",\n            \"description\": \"Flag to indicate if ads should be blocked in the session\"\n          },\n          \"optimizeBandwidth\": {\n            \"anyOf\": [\n              {\n                \"type\": \"boolean\"\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"blockImages\": {\n                    \"type\": \"boolean\"\n                  },\n                  \"blockMedia\": {\n                    \"type\": \"boolean\"\n                  },\n                  \"blockStylesheets\": {\n                    \"type\": \"boolean\"\n                  },\n                  \"blockHosts\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"blockUrlPatterns\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                },\n                \"additionalProperties\": false\n              }\n            ],\n            \"description\": \"Enable bandwidth optimizations. Passing true enables all flags (except hosts/patterns). Object allows granular control.\"\n          },\n          \"skipFingerprintInjection\": {\n            \"type\": \"boolean\",\n            \"description\": \"Flag to indicate if fingerprint injection should be skipped for this session.\"\n          },\n          \"deviceConfig\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"device\": {\n                \"type\": \"string\",\n                \"enum\": [\n                  \"desktop\",\n                  \"mobile\"\n                ],\n                \"default\": \"desktop\"\n              }\n            },\n            \"additionalProperties\": false,\n            \"description\": \"Device configuration for the session. Specify 'mobile' for mobile device fingerprints and configurations.\"\n          },\n          \"logSinkUrl\": {\n            \"type\": \"string\",\n            \"description\": \"Deprecated: Log sink URL to use for the session\"\n          },\n          \"extensions\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Extensions to use for the session\"\n          },\n          \"persist\": {\n            \"type\": \"boolean\",\n            \"description\": \"Flag to indicate if session should be persisted\"\n          },\n          \"userDataDir\": {\n            \"type\": \"string\",\n            \"description\": \"User data directory path to use for the session\"\n          },\n          \"timezone\": {\n            \"type\": \"string\",\n            \"description\": \"Timezone to use for the session\"\n          },\n          \"dimensions\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"width\": {\n                \"type\": \"number\"\n              },\n              \"height\": {\n                \"type\": \"number\"\n              }\n            },\n            \"required\": [\n              \"width\",\n              \"height\"\n            ],\n            \"additionalProperties\": false,\n            \"description\": \"Dimensions to use for the session\"\n          },\n          \"userPreferences\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {},\n            \"description\": \"Chrome user preferences to customize browser behavior (e.g., font size, popup blocking, notification settings)\"\n          },\n          \"extra\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {},\n            \"description\": \"Extra metadata to help initialize the session\"\n          },\n          \"credentials\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"autoSubmit\": {\n                \"anyOf\": [\n                  {\n                    \"type\": \"boolean\"\n                  },\n                  {\n                    \"not\": {}\n                  }\n                ]\n              },\n              \"blurFields\": {\n                \"anyOf\": [\n                  {\n                    \"type\": \"boolean\"\n                  },\n                  {\n                    \"not\": {}\n                  }\n                ]\n              },\n              \"exactOrigin\": {\n                \"anyOf\": [\n                  {\n                    \"type\": \"boolean\"\n                  },\n                  {\n                    \"not\": {}\n                  }\n                ]\n              }\n            },\n            \"additionalProperties\": false,\n            \"description\": \"Configuration for session credentials\"\n          },\n          \"headless\": {\n            \"type\": \"boolean\",\n            \"description\": \"Headless mode for the session\"\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"SessionDetails\": {\n        \"title\": \"SessionDetails\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"format\": \"uuid\",\n            \"description\": \"Unique identifier for the session\"\n          },\n          \"createdAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"description\": \"Timestamp when the session started\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"idle\",\n              \"live\",\n              \"released\",\n              \"failed\"\n            ],\n            \"description\": \"Status of the session\"\n          },\n          \"duration\": {\n            \"type\": \"integer\",\n            \"description\": \"Duration of the session in milliseconds\"\n          },\n          \"eventCount\": {\n            \"type\": \"integer\",\n            \"description\": \"Number of events processed in the session\"\n          },\n          \"dimensions\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"width\": {\n                \"type\": \"number\"\n              },\n              \"height\": {\n                \"type\": \"number\"\n              }\n            },\n            \"required\": [\n              \"width\",\n              \"height\"\n            ],\n            \"additionalProperties\": false,\n            \"description\": \"Dimensions used for the session\"\n          },\n          \"timeout\": {\n            \"type\": \"integer\",\n            \"description\": \"Session timeout duration in milliseconds\"\n          },\n          \"creditsUsed\": {\n            \"type\": \"integer\",\n            \"description\": \"Amount of credits consumed by the session\"\n          },\n          \"websocketUrl\": {\n            \"type\": \"string\",\n            \"description\": \"URL for the session's WebSocket connection\"\n          },\n          \"debugUrl\": {\n            \"type\": \"string\",\n            \"description\": \"URL for a viewing the live browser instance for the session\"\n          },\n          \"debuggerUrl\": {\n            \"type\": \"string\",\n            \"description\": \"URL for debugging the session\"\n          },\n          \"sessionViewerUrl\": {\n            \"type\": \"string\",\n            \"description\": \"URL to view session details\"\n          },\n          \"userAgent\": {\n            \"type\": \"string\",\n            \"description\": \"User agent string used in the session\"\n          },\n          \"proxy\": {\n            \"type\": \"string\",\n            \"description\": \"Proxy server used for the session\"\n          },\n          \"proxyTxBytes\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"description\": \"Amount of data transmitted through the proxy\"\n          },\n          \"proxyRxBytes\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"description\": \"Amount of data received through the proxy\"\n          },\n          \"solveCaptcha\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates if captcha solving is enabled\"\n          },\n          \"isSelenium\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates if Selenium is used in the session\"\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"createdAt\",\n          \"status\",\n          \"duration\",\n          \"eventCount\",\n          \"timeout\",\n          \"creditsUsed\",\n          \"websocketUrl\",\n          \"debugUrl\",\n          \"debuggerUrl\",\n          \"sessionViewerUrl\",\n          \"proxyTxBytes\",\n          \"proxyRxBytes\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"MultipleSessions\": {\n        \"title\": \"MultipleSessions\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"sessions\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"id\": {\n                  \"type\": \"string\",\n                  \"format\": \"uuid\",\n                  \"description\": \"Unique identifier for the session\"\n                },\n                \"createdAt\": {\n                  \"type\": \"string\",\n                  \"format\": \"date-time\",\n                  \"description\": \"Timestamp when the session started\"\n                },\n                \"status\": {\n                  \"type\": \"string\",\n                  \"enum\": [\n                    \"idle\",\n                    \"live\",\n                    \"released\",\n                    \"failed\"\n                  ],\n                  \"description\": \"Status of the session\"\n                },\n                \"duration\": {\n                  \"type\": \"integer\",\n                  \"description\": \"Duration of the session in milliseconds\"\n                },\n                \"eventCount\": {\n                  \"type\": \"integer\",\n                  \"description\": \"Number of events processed in the session\"\n                },\n                \"dimensions\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"width\": {\n                      \"type\": \"number\"\n                    },\n                    \"height\": {\n                      \"type\": \"number\"\n                    }\n                  },\n                  \"required\": [\n                    \"width\",\n                    \"height\"\n                  ],\n                  \"additionalProperties\": false,\n                  \"description\": \"Dimensions used for the session\"\n                },\n                \"timeout\": {\n                  \"type\": \"integer\",\n                  \"description\": \"Session timeout duration in milliseconds\"\n                },\n                \"creditsUsed\": {\n                  \"type\": \"integer\",\n                  \"description\": \"Amount of credits consumed by the session\"\n                },\n                \"websocketUrl\": {\n                  \"type\": \"string\",\n                  \"description\": \"URL for the session's WebSocket connection\"\n                },\n                \"debugUrl\": {\n                  \"type\": \"string\",\n                  \"description\": \"URL for a viewing the live browser instance for the session\"\n                },\n                \"debuggerUrl\": {\n                  \"type\": \"string\",\n                  \"description\": \"URL for debugging the session\"\n                },\n                \"sessionViewerUrl\": {\n                  \"type\": \"string\",\n                  \"description\": \"URL to view session details\"\n                },\n                \"userAgent\": {\n                  \"type\": \"string\",\n                  \"description\": \"User agent string used in the session\"\n                },\n                \"proxy\": {\n                  \"type\": \"string\",\n                  \"description\": \"Proxy server used for the session\"\n                },\n                \"proxyTxBytes\": {\n                  \"type\": \"integer\",\n                  \"minimum\": 0,\n                  \"description\": \"Amount of data transmitted through the proxy\"\n                },\n                \"proxyRxBytes\": {\n                  \"type\": \"integer\",\n                  \"minimum\": 0,\n                  \"description\": \"Amount of data received through the proxy\"\n                },\n                \"solveCaptcha\": {\n                  \"type\": \"boolean\",\n                  \"description\": \"Indicates if captcha solving is enabled\"\n                },\n                \"isSelenium\": {\n                  \"type\": \"boolean\",\n                  \"description\": \"Indicates if Selenium is used in the session\"\n                }\n              },\n              \"required\": [\n                \"id\",\n                \"createdAt\",\n                \"status\",\n                \"duration\",\n                \"eventCount\",\n                \"timeout\",\n                \"creditsUsed\",\n                \"websocketUrl\",\n                \"debugUrl\",\n                \"debuggerUrl\",\n                \"sessionViewerUrl\",\n                \"proxyTxBytes\",\n                \"proxyRxBytes\"\n              ],\n              \"additionalProperties\": false\n            }\n          }\n        },\n        \"required\": [\n          \"sessions\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"SessionContextSchema\": {\n        \"title\": \"SessionContextSchema\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"cookies\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": {\n                  \"type\": \"string\",\n                  \"description\": \"The name of the cookie\"\n                },\n                \"value\": {\n                  \"type\": \"string\",\n                  \"description\": \"The value of the cookie\"\n                },\n                \"url\": {\n                  \"type\": \"string\",\n                  \"description\": \"The URL of the cookie\"\n                },\n                \"domain\": {\n                  \"type\": \"string\",\n                  \"description\": \"The domain of the cookie\"\n                },\n                \"path\": {\n                  \"type\": \"string\",\n                  \"description\": \"The path of the cookie\"\n                },\n                \"secure\": {\n                  \"type\": \"boolean\",\n                  \"description\": \"Whether the cookie is secure\"\n                },\n                \"httpOnly\": {\n                  \"type\": \"boolean\",\n                  \"description\": \"Whether the cookie is HTTP only\"\n                },\n                \"sameSite\": {\n                  \"type\": \"string\",\n                  \"enum\": [\n                    \"Strict\",\n                    \"Lax\",\n                    \"None\"\n                  ],\n                  \"description\": \"The same site attribute of the cookie\"\n                },\n                \"size\": {\n                  \"type\": \"number\",\n                  \"description\": \"The size of the cookie\"\n                },\n                \"expires\": {\n                  \"type\": \"number\",\n                  \"description\": \"The expiration date of the cookie\"\n                },\n                \"partitionKey\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"topLevelSite\": {\n                      \"type\": \"string\",\n                      \"description\": \"The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie.\"\n                    },\n                    \"hasCrossSiteAncestor\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.\"\n                    }\n                  },\n                  \"required\": [\n                    \"topLevelSite\",\n                    \"hasCrossSiteAncestor\"\n                  ],\n                  \"additionalProperties\": false,\n                  \"description\": \"The partition key of the cookie\"\n                },\n                \"session\": {\n                  \"type\": \"boolean\",\n                  \"description\": \"Whether the cookie is a session cookie\"\n                },\n                \"priority\": {\n                  \"type\": \"string\",\n                  \"enum\": [\n                    \"Low\",\n                    \"Medium\",\n                    \"High\"\n                  ],\n                  \"description\": \"The priority of the cookie\"\n                },\n                \"sameParty\": {\n                  \"type\": \"boolean\",\n                  \"description\": \"Whether the cookie is a same party cookie\"\n                },\n                \"sourceScheme\": {\n                  \"type\": \"string\",\n                  \"enum\": [\n                    \"Unset\",\n                    \"NonSecure\",\n                    \"Secure\"\n                  ],\n                  \"description\": \"The source scheme of the cookie\"\n                },\n                \"sourcePort\": {\n                  \"type\": \"number\",\n                  \"description\": \"The source port of the cookie\"\n                }\n              },\n              \"required\": [\n                \"name\",\n                \"value\"\n              ],\n              \"additionalProperties\": false\n            },\n            \"description\": \"Cookies to initialize in the session\"\n          },\n          \"localStorage\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            },\n            \"description\": \"Domain-specific localStorage items to initialize in the session\"\n          },\n          \"sessionStorage\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {\n                \"type\": \"string\"\n              }\n            },\n            \"description\": \"Domain-specific sessionStorage items to initialize in the session\"\n          },\n          \"indexedDB\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"id\": {\n                    \"type\": \"number\"\n                  },\n                  \"name\": {\n                    \"type\": \"string\"\n                  },\n                  \"data\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"id\": {\n                          \"type\": \"number\"\n                        },\n                        \"name\": {\n                          \"type\": \"string\"\n                        },\n                        \"records\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"key\": {},\n                              \"value\": {},\n                              \"blobFiles\": {\n                                \"type\": \"array\",\n                                \"items\": {\n                                  \"type\": \"object\",\n                                  \"properties\": {\n                                    \"blobNumber\": {\n                                      \"type\": \"number\"\n                                    },\n                                    \"mimeType\": {\n                                      \"type\": \"string\"\n                                    },\n                                    \"size\": {\n                                      \"type\": \"number\"\n                                    },\n                                    \"filename\": {\n                                      \"type\": \"string\"\n                                    },\n                                    \"lastModified\": {\n                                      \"type\": \"string\",\n                                      \"format\": \"date-time\"\n                                    },\n                                    \"path\": {\n                                      \"type\": \"string\"\n                                    }\n                                  },\n                                  \"required\": [\n                                    \"blobNumber\",\n                                    \"mimeType\",\n                                    \"size\"\n                                  ],\n                                  \"additionalProperties\": false\n                                }\n                              }\n                            },\n                            \"additionalProperties\": false\n                          }\n                        }\n                      },\n                      \"required\": [\n                        \"id\",\n                        \"name\",\n                        \"records\"\n                      ],\n                      \"additionalProperties\": false\n                    }\n                  }\n                },\n                \"required\": [\n                  \"id\",\n                  \"name\",\n                  \"data\"\n                ],\n                \"additionalProperties\": false\n              }\n            },\n            \"description\": \"Domain-specific indexedDB items to initialize in the session\"\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"RecordedEvents\": {\n        \"title\": \"RecordedEvents\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"events\": {\n            \"type\": \"array\",\n            \"description\": \"Events to emit\"\n          }\n        },\n        \"required\": [\n          \"events\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"ReleaseSession\": {\n        \"title\": \"ReleaseSession\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"format\": \"uuid\",\n            \"description\": \"Unique identifier for the session\"\n          },\n          \"createdAt\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"description\": \"Timestamp when the session started\"\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"idle\",\n              \"live\",\n              \"released\",\n              \"failed\"\n            ],\n            \"description\": \"Status of the session\"\n          },\n          \"duration\": {\n            \"type\": \"integer\",\n            \"description\": \"Duration of the session in milliseconds\"\n          },\n          \"eventCount\": {\n            \"type\": \"integer\",\n            \"description\": \"Number of events processed in the session\"\n          },\n          \"dimensions\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"width\": {\n                \"type\": \"number\"\n              },\n              \"height\": {\n                \"type\": \"number\"\n              }\n            },\n            \"required\": [\n              \"width\",\n              \"height\"\n            ],\n            \"additionalProperties\": false,\n            \"description\": \"Dimensions used for the session\"\n          },\n          \"timeout\": {\n            \"type\": \"integer\",\n            \"description\": \"Session timeout duration in milliseconds\"\n          },\n          \"creditsUsed\": {\n            \"type\": \"integer\",\n            \"description\": \"Amount of credits consumed by the session\"\n          },\n          \"websocketUrl\": {\n            \"type\": \"string\",\n            \"description\": \"URL for the session's WebSocket connection\"\n          },\n          \"debugUrl\": {\n            \"type\": \"string\",\n            \"description\": \"URL for a viewing the live browser instance for the session\"\n          },\n          \"debuggerUrl\": {\n            \"type\": \"string\",\n            \"description\": \"URL for debugging the session\"\n          },\n          \"sessionViewerUrl\": {\n            \"type\": \"string\",\n            \"description\": \"URL to view session details\"\n          },\n          \"userAgent\": {\n            \"type\": \"string\",\n            \"description\": \"User agent string used in the session\"\n          },\n          \"proxy\": {\n            \"type\": \"string\",\n            \"description\": \"Proxy server used for the session\"\n          },\n          \"proxyTxBytes\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"description\": \"Amount of data transmitted through the proxy\"\n          },\n          \"proxyRxBytes\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"description\": \"Amount of data received through the proxy\"\n          },\n          \"solveCaptcha\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates if captcha solving is enabled\"\n          },\n          \"isSelenium\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates if Selenium is used in the session\"\n          },\n          \"success\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates if the session was successfully released\"\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"createdAt\",\n          \"status\",\n          \"duration\",\n          \"eventCount\",\n          \"timeout\",\n          \"creditsUsed\",\n          \"websocketUrl\",\n          \"debugUrl\",\n          \"debuggerUrl\",\n          \"sessionViewerUrl\",\n          \"proxyTxBytes\",\n          \"proxyRxBytes\",\n          \"success\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"SessionStreamQuery\": {\n        \"title\": \"SessionStreamQuery\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"showControls\": {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Show controls in the browser iframe\"\n          },\n          \"theme\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"dark\",\n              \"light\"\n            ],\n            \"default\": \"dark\",\n            \"description\": \"Theme of the browser iframe\"\n          },\n          \"interactive\": {\n            \"type\": \"boolean\",\n            \"default\": true,\n            \"description\": \"Make the browser iframe interactive\"\n          },\n          \"pageId\": {\n            \"type\": \"string\",\n            \"description\": \"Page ID to connect to\"\n          },\n          \"pageIndex\": {\n            \"type\": \"string\",\n            \"description\": \"Page index (or tab index) to connect to\"\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"SessionStreamResponse\": {\n        \"title\": \"SessionStreamResponse\",\n        \"type\": \"string\",\n        \"description\": \"HTML content for the session streamer view\"\n      },\n      \"SessionLiveDetailsResponse\": {\n        \"title\": \"SessionLiveDetailsResponse\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"sessionViewerUrl\": {\n            \"type\": \"string\"\n          },\n          \"sessionViewerFullscreenUrl\": {\n            \"type\": \"string\"\n          },\n          \"websocketUrl\": {\n            \"type\": \"string\"\n          },\n          \"pages\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"id\": {\n                  \"type\": \"string\"\n                },\n                \"url\": {\n                  \"type\": \"string\"\n                },\n                \"title\": {\n                  \"type\": \"string\"\n                },\n                \"favicon\": {\n                  \"type\": \"string\",\n                  \"nullable\": true\n                }\n              },\n              \"required\": [\n                \"id\",\n                \"url\",\n                \"title\",\n                \"favicon\"\n              ],\n              \"additionalProperties\": false\n            }\n          },\n          \"browserState\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"status\": {\n                \"type\": \"string\",\n                \"enum\": [\n                  \"idle\",\n                  \"live\",\n                  \"released\",\n                  \"failed\"\n                ]\n              },\n              \"userAgent\": {\n                \"type\": \"string\"\n              },\n              \"browserVersion\": {\n                \"type\": \"string\"\n              },\n              \"initialDimensions\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"width\": {\n                    \"type\": \"number\"\n                  },\n                  \"height\": {\n                    \"type\": \"number\"\n                  }\n                },\n                \"required\": [\n                  \"width\",\n                  \"height\"\n                ],\n                \"additionalProperties\": false\n              },\n              \"pageCount\": {\n                \"type\": \"number\"\n              }\n            },\n            \"required\": [\n              \"status\",\n              \"userAgent\",\n              \"browserVersion\",\n              \"initialDimensions\",\n              \"pageCount\"\n            ],\n            \"additionalProperties\": false\n          }\n        },\n        \"required\": [\n          \"sessionViewerUrl\",\n          \"sessionViewerFullscreenUrl\",\n          \"websocketUrl\",\n          \"pages\",\n          \"browserState\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"LogQuerySchema\": {\n        \"title\": \"LogQuerySchema\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"startTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"endTime\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"eventTypes\": {\n            \"type\": \"string\"\n          },\n          \"pageId\": {\n            \"type\": \"string\"\n          },\n          \"targetType\": {\n            \"type\": \"string\"\n          },\n          \"limit\": {\n            \"type\": \"integer\",\n            \"minimum\": 1,\n            \"maximum\": 1000,\n            \"default\": 100\n          },\n          \"offset\": {\n            \"type\": \"integer\",\n            \"minimum\": 0,\n            \"default\": 0\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"LogStatsSchema\": {\n        \"title\": \"LogStatsSchema\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"totalEvents\": {\n            \"type\": \"number\"\n          },\n          \"oldestEvent\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"newestEvent\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"nullable\": true\n          },\n          \"sizeBytes\": {\n            \"type\": \"number\"\n          }\n        },\n        \"required\": [\n          \"totalEvents\",\n          \"oldestEvent\",\n          \"newestEvent\",\n          \"sizeBytes\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"LogQueryResultSchema\": {\n        \"title\": \"LogQueryResultSchema\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"events\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": {}\n            }\n          },\n          \"total\": {\n            \"type\": \"number\"\n          },\n          \"hasMore\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"required\": [\n          \"events\",\n          \"total\",\n          \"hasMore\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"ExportLogsSchema\": {\n        \"title\": \"ExportLogsSchema\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"query\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"startTime\": {\n                \"type\": \"string\",\n                \"format\": \"date-time\"\n              },\n              \"endTime\": {\n                \"type\": \"string\",\n                \"format\": \"date-time\"\n              },\n              \"eventTypes\": {\n                \"type\": \"string\"\n              },\n              \"pageId\": {\n                \"type\": \"string\"\n              },\n              \"targetType\": {\n                \"type\": \"string\"\n              },\n              \"limit\": {\n                \"type\": \"integer\",\n                \"minimum\": 1,\n                \"maximum\": 1000,\n                \"default\": 100\n              },\n              \"offset\": {\n                \"type\": \"integer\",\n                \"minimum\": 0,\n                \"default\": 0\n              }\n            },\n            \"additionalProperties\": false\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"GetDevtoolsUrlSchema\": {\n        \"title\": \"GetDevtoolsUrlSchema\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"pageId\": {\n            \"type\": \"string\"\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"LaunchRequest\": {\n        \"title\": \"LaunchRequest\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"options\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"args\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"string\"\n                }\n              },\n              \"chromiumSandbox\": {\n                \"type\": \"boolean\"\n              },\n              \"devtools\": {\n                \"type\": \"boolean\"\n              },\n              \"downloadsPath\": {\n                \"type\": \"string\"\n              },\n              \"headless\": {\n                \"type\": \"boolean\"\n              },\n              \"ignoreDefaultArgs\": {\n                \"anyOf\": [\n                  {\n                    \"type\": \"boolean\"\n                  },\n                  {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                ]\n              },\n              \"proxyUrl\": {\n                \"type\": \"string\"\n              },\n              \"timeout\": {\n                \"type\": \"number\"\n              },\n              \"tracesDir\": {\n                \"type\": \"string\"\n              }\n            },\n            \"additionalProperties\": false\n          },\n          \"req\": {},\n          \"stealth\": {\n            \"type\": \"boolean\"\n          },\n          \"cookies\": {\n            \"type\": \"array\"\n          },\n          \"userAgent\": {\n            \"type\": \"string\"\n          },\n          \"extensions\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          },\n          \"logSinkUrl\": {\n            \"type\": \"string\",\n            \"description\": \"Deprecated\"\n          },\n          \"customHeaders\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"timezone\": {\n            \"type\": \"string\"\n          },\n          \"dimensions\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"width\": {\n                \"type\": \"number\"\n              },\n              \"height\": {\n                \"type\": \"number\"\n              }\n            },\n            \"required\": [\n              \"width\",\n              \"height\"\n            ],\n            \"additionalProperties\": false,\n            \"nullable\": true\n          }\n        },\n        \"required\": [\n          \"options\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"LaunchResponse\": {\n        \"title\": \"LaunchResponse\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"success\": {\n            \"type\": \"boolean\"\n          }\n        },\n        \"required\": [\n          \"success\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"FileUploadRequest\": {\n        \"title\": \"FileUploadRequest\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"file\": {\n            \"description\": \"The file to upload (binary) or URL string to download from\"\n          },\n          \"path\": {\n            \"type\": \"string\",\n            \"description\": \"Path to the file in the storage system\"\n          }\n        },\n        \"additionalProperties\": false\n      },\n      \"FileDetails\": {\n        \"title\": \"FileDetails\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"path\": {\n            \"type\": \"string\",\n            \"description\": \"Path to the file in the storage system\"\n          },\n          \"size\": {\n            \"type\": \"number\",\n            \"description\": \"Size of the file in bytes\"\n          },\n          \"lastModified\": {\n            \"type\": \"string\",\n            \"format\": \"date-time\",\n            \"description\": \"Timestamp when the file was last updated\"\n          }\n        },\n        \"required\": [\n          \"path\",\n          \"size\",\n          \"lastModified\"\n        ],\n        \"additionalProperties\": false\n      },\n      \"MultipleFiles\": {\n        \"title\": \"MultipleFiles\",\n        \"type\": \"object\",\n        \"properties\": {\n          \"data\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"path\": {\n                  \"type\": \"string\",\n                  \"description\": \"Path to the file in the storage system\"\n                },\n                \"size\": {\n                  \"type\": \"number\",\n                  \"description\": \"Size of the file in bytes\"\n                },\n                \"lastModified\": {\n                  \"type\": \"string\",\n                  \"format\": \"date-time\",\n                  \"description\": \"Timestamp when the file was last updated\"\n                }\n              },\n              \"required\": [\n                \"path\",\n                \"size\",\n                \"lastModified\"\n              ],\n              \"additionalProperties\": false\n            },\n            \"description\": \"Array of files for the current page\"\n          }\n        },\n        \"required\": [\n          \"data\"\n        ],\n        \"additionalProperties\": false\n      }\n    }\n  },\n  \"paths\": {\n    \"/v1/scrape\": {\n      \"post\": {\n        \"operationId\": \"scrape\",\n        \"summary\": \"Scrape a URL\",\n        \"tags\": [\n          \"Browser Actions\"\n        ],\n        \"description\": \"Scrape a URL\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ScrapeRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ScrapeResponse\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/screenshot\": {\n      \"post\": {\n        \"operationId\": \"screenshot\",\n        \"summary\": \"Take a screenshot\",\n        \"tags\": [\n          \"Browser Actions\"\n        ],\n        \"description\": \"Take a screenshot\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ScreenshotRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ScreenshotResponse\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/pdf\": {\n      \"post\": {\n        \"operationId\": \"pdf\",\n        \"summary\": \"Get the PDF content of a page\",\n        \"tags\": [\n          \"Browser Actions\"\n        ],\n        \"description\": \"Get the PDF content of a page\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PDFRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PDFResponse\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/health\": {\n      \"get\": {\n        \"operationId\": \"health\",\n        \"summary\": \"Check if the server and browser are running\",\n        \"tags\": [\n          \"Health\"\n        ],\n        \"description\": \"Check if the server and browser are running\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    },\n    \"/v1/sessions\": {\n      \"post\": {\n        \"operationId\": \"launch_browser_session\",\n        \"summary\": \"Launch a browser session\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Launch a browser session\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/CreateSession\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SessionDetails\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"get\": {\n        \"operationId\": \"get_sessions\",\n        \"summary\": \"Get all sessions\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Get all sessions\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MultipleSessions\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/sessions/{sessionId}\": {\n      \"get\": {\n        \"operationId\": \"get_session_details\",\n        \"summary\": \"Get session details\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Get session details\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SessionDetails\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/sessions/{sessionId}/context\": {\n      \"get\": {\n        \"operationId\": \"get_browser_context\",\n        \"summary\": \"Get a browser context\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Get a browser context\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SessionContextSchema\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/sessions/{sessionId}/release\": {\n      \"post\": {\n        \"operationId\": \"release_browser_session\",\n        \"summary\": \"Release a browser session\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Release a browser session\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReleaseSession\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/sessions/release\": {\n      \"post\": {\n        \"operationId\": \"release_browser_sessions\",\n        \"summary\": \"Release browser sessions\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Release browser sessions\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ReleaseSession\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/sessions/debug\": {\n      \"get\": {\n        \"operationId\": \"get_session_debugger_stream\",\n        \"summary\": \"Get session debugger view\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Returns an HTML page with a live debugger view of the session\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"default\": true\n            },\n            \"in\": \"query\",\n            \"name\": \"showControls\",\n            \"required\": false,\n            \"description\": \"Show controls in the browser iframe\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"dark\",\n                \"light\"\n              ],\n              \"default\": \"dark\"\n            },\n            \"in\": \"query\",\n            \"name\": \"theme\",\n            \"required\": false,\n            \"description\": \"Theme of the browser iframe\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"default\": true\n            },\n            \"in\": \"query\",\n            \"name\": \"interactive\",\n            \"required\": false,\n            \"description\": \"Make the browser iframe interactive\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"pageId\",\n            \"required\": false,\n            \"description\": \"Page ID to connect to\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"pageIndex\",\n            \"required\": false,\n            \"description\": \"Page index (or tab index) to connect to\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"HTML content for the session streamer view\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SessionStreamResponse\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/events\": {\n      \"post\": {\n        \"operationId\": \"receive_events\",\n        \"summary\": \"Receive recorded events from the browser\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Receive recorded events from the browser\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/RecordedEvents\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    },\n    \"/v1/sessions/{id}/live-details\": {\n      \"get\": {\n        \"operationId\": \"get_session_live_details\",\n        \"summary\": \"Get session live details\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Returns the live state of the session, including pages, tabs, and browser state\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"id\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/SessionLiveDetailsResponse\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/sessions/scrape\": {\n      \"post\": {\n        \"operationId\": \"scrape_session\",\n        \"summary\": \"Scrape Current Session\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Scrape Current Session\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ScrapeRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ScrapeResponse\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/sessions/screenshot\": {\n      \"post\": {\n        \"operationId\": \"screenshot_session\",\n        \"summary\": \"Take Screenshot of Current Session\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Take Screenshot of Current Session\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/ScreenshotRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/ScreenshotResponse\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/sessions/pdf\": {\n      \"post\": {\n        \"operationId\": \"pdf_session\",\n        \"summary\": \"Generate PDF of Current Session\",\n        \"tags\": [\n          \"Sessions\"\n        ],\n        \"description\": \"Generate PDF of Current Session\",\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/PDFRequest\"\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PDFResponse\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/devtools/inspector.html\": {\n      \"get\": {\n        \"operationId\": \"getDevtoolsUrl\",\n        \"summary\": \"Get the URL for the DevTools inspector\",\n        \"tags\": [\n          \"CDP\"\n        ],\n        \"description\": \"Get the URL for the DevTools inspector\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"pageId\",\n            \"required\": false\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    },\n    \"/v1/sessions/{sessionId}/files\": {\n      \"post\": {\n        \"operationId\": \"upload_file\",\n        \"summary\": \"Upload a file\",\n        \"tags\": [\n          \"Files\"\n        ],\n        \"description\": \"Uploads a file to a session via `multipart/form-data` with a `file` field that accepts either binary data or a URL string to download from, and an optional `path` field for the file storage path.\",\n        \"requestBody\": {\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"$ref\": \"#/components/schemas/FileUploadRequest\"\n              }\n            }\n          }\n        },\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/FileDetails\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"get\": {\n        \"operationId\": \"list_files\",\n        \"summary\": \"List files\",\n        \"tags\": [\n          \"Files\"\n        ],\n        \"description\": \"List all files from the session in descending order.\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/MultipleFiles\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"delete_all_files\",\n        \"summary\": \"Delete all files\",\n        \"tags\": [\n          \"Files\"\n        ],\n        \"description\": \"Delete all files from a session\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content\"\n          }\n        }\n      }\n    },\n    \"/v1/sessions/{sessionId}/files/{*}\": {\n      \"get\": {\n        \"operationId\": \"download_file\",\n        \"summary\": \"Download a file\",\n        \"tags\": [\n          \"Files\"\n        ],\n        \"description\": \"Download a file from a session\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"*\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"delete_file\",\n        \"summary\": \"Delete a file\",\n        \"tags\": [\n          \"Files\"\n        ],\n        \"description\": \"Delete a file from a session\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"*\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content\"\n          }\n        }\n      }\n    },\n    \"/v1/sessions/{sessionId}/files.zip\": {\n      \"get\": {\n        \"operationId\": \"download_archive\",\n        \"summary\": \"Download archive\",\n        \"tags\": [\n          \"Files\"\n        ],\n        \"description\": \"Download all files from the session as a zip archive.\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"path\",\n            \"name\": \"sessionId\",\n            \"required\": true\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    },\n    \"/v1/logs/query\": {\n      \"get\": {\n        \"tags\": [\n          \"Logs\"\n        ],\n        \"description\": \"Query browser logs from local storage\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"in\": \"query\",\n            \"name\": \"startTime\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"in\": \"query\",\n            \"name\": \"endTime\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"eventTypes\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"pageId\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"targetType\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"integer\",\n              \"minimum\": 1,\n              \"maximum\": 1000,\n              \"default\": 100\n            },\n            \"in\": \"query\",\n            \"name\": \"limit\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"integer\",\n              \"minimum\": 0,\n              \"default\": 0\n            },\n            \"in\": \"query\",\n            \"name\": \"offset\",\n            \"required\": false\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    },\n    \"/v1/logs/stats\": {\n      \"get\": {\n        \"tags\": [\n          \"Logs\"\n        ],\n        \"description\": \"Get statistics about stored browser logs\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    },\n    \"/v1/logs/stream\": {\n      \"get\": {\n        \"tags\": [\n          \"Logs\"\n        ],\n        \"description\": \"Stream browser logs in real-time using SSE\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    },\n    \"/v1/logs/export\": {\n      \"post\": {\n        \"tags\": [\n          \"Logs\"\n        ],\n        \"description\": \"Export browser logs to Parquet format\",\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"in\": \"query\",\n            \"name\": \"startTime\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"format\": \"date-time\"\n            },\n            \"in\": \"query\",\n            \"name\": \"endTime\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"eventTypes\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"pageId\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"in\": \"query\",\n            \"name\": \"targetType\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"integer\",\n              \"minimum\": 1,\n              \"maximum\": 1000,\n              \"default\": 100\n            },\n            \"in\": \"query\",\n            \"name\": \"limit\",\n            \"required\": false\n          },\n          {\n            \"schema\": {\n              \"type\": \"integer\",\n              \"minimum\": 0,\n              \"default\": 0\n            },\n            \"in\": \"query\",\n            \"name\": \"offset\",\n            \"required\": false\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    },\n    \"/v1/logs/\": {\n      \"delete\": {\n        \"tags\": [\n          \"Logs\"\n        ],\n        \"description\": \"Clear all browser logs from storage\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Default Response\"\n          }\n        }\n      }\n    }\n  },\n  \"servers\": [\n    {\n      \"url\": \"http://0.0.0.0:3000/\",\n      \"description\": \"Local server\"\n    },\n    {\n      \"url\": \"http://0.0.0.0:3000\",\n      \"description\": \"Local server from env variables\"\n    }\n  ]\n}"
  },
  {
    "path": "api/package.json",
    "content": "{\n  \"name\": \"@steel-browser/api\",\n  \"version\": \"0.5.1\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"type\": \"module\",\n  \"private\": true,\n  \"exports\": {\n    \"./plugin\": {\n      \"import\": {\n        \"types\": \"./build/steel-browser-plugin.d.ts\",\n        \"default\": \"./build/steel-browser-plugin.js\"\n      }\n    },\n    \"./cdp-plugin\": {\n      \"import\": {\n        \"types\": \"./build/services/cdp/plugins/core/base-plugin.d.ts\",\n        \"default\": \"./build/services/cdp/plugins/core/base-plugin.js\"\n      }\n    },\n    \"./logger\": {\n      \"import\": {\n        \"types\": \"./build/plugins/logging/browser-logger.d.ts\",\n        \"default\": \"./build/plugins/logging/browser-logger.js\"\n      }\n    },\n    \"./storage\": {\n      \"import\": {\n        \"types\": \"./build/services/cdp/instrumentation/storage/log-storage.interface.d.ts\",\n        \"default\": \"./build/services/cdp/instrumentation/storage/log-storage.interface.js\"\n      }\n    }\n  },\n  \"scripts\": {\n    \"start\": \"node ./build/index.js\",\n    \"build\": \"tsc && npm run copy:templates && npm run copy:fingerprint\",\n    \"lint\": \"eslint . --ext ts --report-unused-disable-directives --max-warnings 10\",\n    \"copy:templates\": \"mkdir -p build/templates && cp -r src/templates/* build/templates/\",\n    \"copy:fingerprint\": \"cp src/scripts/fingerprint.js build/scripts/fingerprint.js\",\n    \"prepare:recorder\": \"cd extensions/recorder && npm install && npm run build\",\n    \"dev\": \"npm run prepare:recorder && tsx watch src/index.ts\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"pretty\": \"prettier --write \\\"src/**/*.ts\\\"\",\n    \"generate:openapi\": \"tsx ./openapi/generate.ts\"\n  },\n  \"author\": \"Nasr Mohamed\",\n  \"devDependencies\": {\n    \"@opentelemetry/api\": \"1.9.0\",\n    \"@types/archiver\": \"^6.0.3\",\n    \"@types/iconv-lite\": \"^0.0.1\",\n    \"@types/json-stringify-safe\": \"^5.0.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/markdownlint\": \"^0.13.0\",\n    \"@types/mime-types\": \"^2.1.4\",\n    \"@types/node\": \"^22.14.1\",\n    \"@types/turndown\": \"^5.0.5\",\n    \"@types/ws\": \"^8.5.14\",\n    \"fastify\": \"^5.2.1\",\n    \"prettier\": \"3.0.3\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsx\": \"^4.19.3\",\n    \"typescript\": \"^5.7.3\",\n    \"vite\": \"^6.3.5\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"dependencies\": {\n    \"@adobe/css-tools\": \"^4.4.3\",\n    \"@fastify/cors\": \"^10.0.2\",\n    \"@fastify/multipart\": \"^9.0.3\",\n    \"@fastify/reply-from\": \"^12.0.2\",\n    \"@fastify/sensible\": \"^6.0.3\",\n    \"@fastify/static\": \"^8.1.1\",\n    \"@fastify/swagger\": \"^9.4.2\",\n    \"@fastify/view\": \"10.0.2\",\n    \"@joplin/turndown\": \"^4.0.80\",\n    \"@scalar/fastify-api-reference\": \"^1.25.116\",\n    \"archiver\": \"^7.0.1\",\n    \"axios\": \"^1.12.0\",\n    \"cheerio\": \"^1.1.2\",\n    \"chokidar\": \"^4.0.3\",\n    \"defuddle\": \"^0.6.4\",\n    \"dotenv\": \"^16.4.7\",\n    \"duckdb-async\": \"^1.1.3\",\n    \"ejs\": \"^3.1.10\",\n    \"fastify-plugin\": \"^5.0.1\",\n    \"file-type\": \"^20.4.1\",\n    \"fingerprint-generator\": \"2.1.72\",\n    \"fingerprint-injector\": \"2.1.72\",\n    \"http-proxy\": \"^1.18.1\",\n    \"https-proxy-agent\": \"^7.0.6\",\n    \"iconv-lite\": \"^0.6.3\",\n    \"jsdom\": \"^24.1.3\",\n    \"json-stringify-safe\": \"^5.0.1\",\n    \"level\": \"^9.0.0\",\n    \"lodash-es\": \"^4.17.21\",\n    \"markdownlint\": \"^0.38.0\",\n    \"mime-types\": \"^2.1.35\",\n    \"pdf2html\": \"^4.4.0\",\n    \"pino\": \"^9.6.0\",\n    \"pino-pretty\": \"^13.0.0\",\n    \"proxy-chain\": \"^2.5.6\",\n    \"puppeteer-core\": \"23.6.0\",\n    \"socks-proxy-agent\": \"^8.0.5\",\n    \"uuid\": \"^11.0.5\",\n    \"zod\": \"^3.24.2\",\n    \"zod-to-json-schema\": \"^3.24.1\"\n  },\n  \"overrides\": {\n    \"cross-spawn\": \"^7.0.6\",\n    \"tar-fs\": \">=3.1.1\"\n  },\n  \"peerDependencies\": {\n    \"@opentelemetry/api\": \"1.9.0\",\n    \"fastify\": \"^5.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"fastify\": {\n      \"optional\": false\n    },\n    \"@opentelemetry/api\": {\n      \"optional\": true\n    }\n  }\n}"
  },
  {
    "path": "api/selenium/driver/LICENSE.chromedriver",
    "content": "// Copyright 2015 The Chromium Authors\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n//    * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n//    * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n//    * Neither the name of Google LLC nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "api/selenium/driver/THIRD_PARTY_NOTICES.chromedriver",
    "content": "--------------------\nLicense notice for Google Double Conversion\n--------------------\nCopyright 2006-2011, the V8 project authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for QUICHE\n--------------------\n// Copyright 2015 The Chromium Authors. All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n//    * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n//    * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n//    * Neither the name of Google Inc. nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for Abseil\n--------------------\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        https://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       https://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n\n--------------------\nLicense notice for Implementation of WebDriver BiDi standard\n--------------------\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n--------------------\nLicense notice for mitt\n--------------------\nMIT License\n\nCopyright (c) 2021 Jason Miller\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n--------------------\nLicense notice for urlpattern-polyfill\n--------------------\nCopyright 2020 Intel Corporation\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n--------------------\nLicense notice for zod\n--------------------\nMIT License\n\nCopyright (c) 2020 Colin McDonnell\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n\n--------------------\nLicense notice for WebKit\n--------------------\n(WebKit doesn't distribute an explicit license.  This LICENSE is derived from\nlicense text in the source.)\n\nCopyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,\n2006, 2007 Alexander Kellett, Alexey Proskuryakov, Alex Mathews, Allan\nSandfeld Jensen, Alp Toker, Anders Carlsson, Andrew Wellington, Antti\nKoivisto, Apple Inc., Arthur Langereis, Baron Schwartz, Bjoern Graf,\nBrent Fulgham, Cameron Zwarich, Charles Samuels, Christian Dywan,\nCollabora Ltd., Cyrus Patel, Daniel Molkentin, Dave Maclachlan, David\nSmith, Dawit Alemayehu, Dirk Mueller, Dirk Schulze, Don Gibson, Enrico\nRos, Eric Seidel, Frederik Holljen, Frerich Raabe, Friedmann Kleint,\nGeorge Staikos, Google Inc., Graham Dennis, Harri Porten, Henry Mason,\nHiroyuki Ikezoe, Holger Hans Peter Freyther, IBM, James G. Speth, Jan\nAlonzo, Jean-Loup Gailly, John Reis, Jonas Witt, Jon Shier, Jonas\nWitt, Julien Chaffraix, Justin Haygood, Kevin Ollivier, Kevin Watters,\nKimmo Kinnunen, Kouhei Sutou, Krzysztof Kowalczyk, Lars Knoll, Luca\nBruno, Maks Orlovich, Malte Starostik, Mark Adler, Martin Jones,\nMarvin Decker, Matt Lilek, Michael Emmel, Mitz Pettel, mozilla.org,\nNetscape Communications Corporation, Nicholas Shanks, Nikolas\nZimmermann, Nokia, Oliver Hunt, Opened Hand, Paul Johnston, Peter\nKelly, Pioneer Research Center USA, Rich Moore, Rob Buis, Robin Dunn,\nRonald Tschalär, Samuel Weinig, Simon Hausmann, Staikos Computing\nServices Inc., Stefan Schimanski, Symantec Corporation, The Dojo\nFoundation, The Karbon Developers, Thomas Boyer, Tim Copperfield,\nTobias Anton, Torben Weis, Trolltech, University of Cambridge, Vaclav\nSlavik, Waldo Bastian, Xan Lopez, Zack Rusin\n\nThe terms and conditions vary from file to file, but are one of:\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n1. Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the\n   distribution.\n\n*OR*\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n1. Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the\n   distribution.\n3. Neither the name of Apple Computer, Inc. (\"Apple\") nor the names of\n   its contributors may be used to endorse or promote products derived\n   from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY\nEXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY\n\nOF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n                  GNU LIBRARY GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1991 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the library GPL.  It is\n numbered 2 because it goes with version 2 of the ordinary GPL.]\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Library General Public License, applies to some\nspecially designated Free Software Foundation software, and to any\nother libraries whose authors decide to use it.  You can use it for\nyour libraries, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if\nyou distribute copies of the library, or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link a program with the library, you must provide\ncomplete object files to the recipients so that they can relink them\nwith the library, after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  Our method of protecting your rights has two steps: (1) copyright\nthe library, and (2) offer you this license which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  Also, for each distributor's protection, we want to make certain\nthat everyone understands that there is no warranty for this free\nlibrary.  If the library is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original\nversion, so that any problems introduced by others will not reflect on\nthe original authors' reputations.\n\f\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that companies distributing free\nsoftware will individually obtain patent licenses, thus in effect\ntransforming the program into proprietary software.  To prevent this,\nwe have made it clear that any patent must be licensed for everyone's\nfree use or not licensed at all.\n\n  Most GNU software, including some libraries, is covered by the ordinary\nGNU General Public License, which was designed for utility programs.  This\nlicense, the GNU Library General Public License, applies to certain\ndesignated libraries.  This license is quite different from the ordinary\none; be sure to read it in full, and don't assume that anything in it is\nthe same as in the ordinary license.\n\n  The reason we have a separate public license for some libraries is that\nthey blur the distinction we usually make between modifying or adding to a\nprogram and simply using it.  Linking a program with a library, without\nchanging the library, is in some sense simply using the library, and is\nanalogous to running a utility program or application program.  However, in\na textual and legal sense, the linked executable is a combined work, a\nderivative of the original library, and the ordinary General Public License\ntreats it as such.\n\n  Because of this blurred distinction, using the ordinary General\nPublic License for libraries did not effectively promote software\nsharing, because most developers did not use the libraries.  We\nconcluded that weaker conditions might promote sharing better.\n\n  However, unrestricted linking of non-free programs would deprive the\nusers of those programs of all benefit from the free status of the\nlibraries themselves.  This Library General Public License is intended to\npermit developers of non-free programs to use free libraries, while\npreserving your freedom as a user of such programs to change the free\nlibraries that are incorporated in them.  (We have not seen how to achieve\nthis as regards changes in header files, but we have achieved it as regards\nchanges in the actual functions of the Library.)  The hope is that this\nwill lead to faster development of free libraries.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, while the latter only\nworks together with the library.\n\n  Note that it is possible for a library to be covered by the ordinary\nGeneral Public License rather than by this special one.\n\f\n                  GNU LIBRARY GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library which\ncontains a notice placed by the copyright holder or other authorized\nparty saying it may be distributed under the terms of this Library\nGeneral Public License (also called \"this License\").  Each licensee is\naddressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n  \n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also compile or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    c) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    d) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe source code distributed need not include anything that is normally\ndistributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Library General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n                            NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n                  GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\f\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\f\n                  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n\n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\f\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\f\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\f\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\f\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\f\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\f\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n                            NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for BoringSSL\n--------------------\nBoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL\nlicensing. Files that are completely new have a Google copyright and an ISC\nlicense. This license is reproduced at the bottom of this file.\n\nContributors to BoringSSL are required to follow the CLA rules for Chromium:\nhttps://cla.developers.google.com/clas\n\nFiles in third_party/ have their own licenses, as described therein. The MIT\nlicense, for third_party/fiat, which, unlike other third_party directories, is\ncompiled into non-test libraries, is included below.\n\nThe OpenSSL toolkit stays under a dual license, i.e. both the conditions of the\nOpenSSL License and the original SSLeay license apply to the toolkit. See below\nfor the actual license texts. Actually both licenses are BSD-style Open Source\nlicenses. In case of any license issues related to OpenSSL please contact\nopenssl-core@openssl.org.\n\nThe following are Google-internal bug numbers where explicit permission from\nsome authors is recorded for use of their work. (This is purely for our own\nrecord keeping.)\n  27287199\n  27287880\n  27287883\n  263291445\n\n  OpenSSL License\n  ---------------\n\n/* ====================================================================\n * Copyright (c) 1998-2011 The OpenSSL Project.  All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n *\n * 1. Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer. \n *\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in\n *    the documentation and/or other materials provided with the\n *    distribution.\n *\n * 3. All advertising materials mentioning features or use of this\n *    software must display the following acknowledgment:\n *    \"This product includes software developed by the OpenSSL Project\n *    for use in the OpenSSL Toolkit. (http://www.openssl.org/)\"\n *\n * 4. The names \"OpenSSL Toolkit\" and \"OpenSSL Project\" must not be used to\n *    endorse or promote products derived from this software without\n *    prior written permission. For written permission, please contact\n *    openssl-core@openssl.org.\n *\n * 5. Products derived from this software may not be called \"OpenSSL\"\n *    nor may \"OpenSSL\" appear in their names without prior written\n *    permission of the OpenSSL Project.\n *\n * 6. Redistributions of any form whatsoever must retain the following\n *    acknowledgment:\n *    \"This product includes software developed by the OpenSSL Project\n *    for use in the OpenSSL Toolkit (http://www.openssl.org/)\"\n *\n * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY\n * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\n * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE OpenSSL PROJECT OR\n * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\n * OF THE POSSIBILITY OF SUCH DAMAGE.\n * ====================================================================\n *\n * This product includes cryptographic software written by Eric Young\n * (eay@cryptsoft.com).  This product includes software written by Tim\n * Hudson (tjh@cryptsoft.com).\n *\n */\n\n Original SSLeay License\n -----------------------\n\n/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)\n * All rights reserved.\n *\n * This package is an SSL implementation written\n * by Eric Young (eay@cryptsoft.com).\n * The implementation was written so as to conform with Netscapes SSL.\n * \n * This library is free for commercial and non-commercial use as long as\n * the following conditions are aheared to.  The following conditions\n * apply to all code found in this distribution, be it the RC4, RSA,\n * lhash, DES, etc., code; not just the SSL code.  The SSL documentation\n * included with this distribution is covered by the same copyright terms\n * except that the holder is Tim Hudson (tjh@cryptsoft.com).\n * \n * Copyright remains Eric Young's, and as such any Copyright notices in\n * the code are not to be removed.\n * If this package is used in a product, Eric Young should be given attribution\n * as the author of the parts of the library used.\n * This can be in the form of a textual message at program startup or\n * in documentation (online or textual) provided with the package.\n * \n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the copyright\n *    notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n *    notice, this list of conditions and the following disclaimer in the\n *    documentation and/or other materials provided with the distribution.\n * 3. All advertising materials mentioning features or use of this software\n *    must display the following acknowledgement:\n *    \"This product includes cryptographic software written by\n *     Eric Young (eay@cryptsoft.com)\"\n *    The word 'cryptographic' can be left out if the rouines from the library\n *    being used are not cryptographic related :-).\n * 4. If you include any Windows specific code (or a derivative thereof) from \n *    the apps directory (application code) you must include an acknowledgement:\n *    \"This product includes software written by Tim Hudson (tjh@cryptsoft.com)\"\n * \n * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\n * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\n * SUCH DAMAGE.\n * \n * The licence and distribution terms for any publically available version or\n * derivative of this code cannot be changed.  i.e. this code cannot simply be\n * copied and put under another distribution licence\n * [including the GNU Public Licence.]\n */\n\n\nISC license used for completely new code in BoringSSL:\n\n/* Copyright (c) 2015, Google Inc.\n *\n * Permission to use, copy, modify, and/or distribute this software for any\n * purpose with or without fee is hereby granted, provided that the above\n * copyright notice and this permission notice appear in all copies.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\n * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION\n * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN\n * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */\n\n\nThe code in third_party/fiat carries the MIT license:\n\nCopyright (c) 2015-2016 the fiat-crypto authors (see\nhttps://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS).\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\nLicenses for support code\n-------------------------\n\nParts of the TLS test suite are under the Go license. This code is not included\nin BoringSSL (i.e. libcrypto and libssl) when compiled, however, so\ndistributing code linked against BoringSSL does not trigger this license:\n\nCopyright (c) 2009 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\nBoringSSL uses the Chromium test infrastructure to run a continuous build,\ntrybots etc. The scripts which manage this, and the script for generating build\nmetadata, are under the Chromium license. Distributing code linked against\nBoringSSL does not trigger this license.\n\nCopyright 2015 The Chromium Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for Fiat-Crypto: Synthesizing Correct-by-Construction Code for Cryptographic Primitives\n--------------------\nThe MIT License (MIT)\n\nCopyright (c) 2015-2020 the fiat-crypto authors (see\nhttps://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS).\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n--------------------\nLicense notice for Brotli\n--------------------\nCopyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n--------------------\nLicense notice for Compact Encoding Detection\n--------------------\n// Copyright 2010 The Chromium Authors\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n//    * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n//    * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n//    * Neither the name of Google LLC nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for Crashpad\n--------------------\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------\nLicense notice for CRC32C\n--------------------\nCopyright 2017, The CRC32C Authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for d3\n--------------------\nCopyright 2010-2023 Mike Bostock\n\nPermission to use, copy, modify, and/or distribute this software for any purpose\nwith or without fee is hereby granted, provided that the above copyright notice\nand this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\nOF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\nTORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF\nTHIS SOFTWARE.\n\n--------------------\nLicense notice for dav1d is an AV1 decoder :)\n--------------------\nCopyright © 2018, VideoLAN and dav1d authors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for Dawn\n--------------------\n// Copyright 2017-2023 The Dawn & Tint Authors\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are met:\n//\n// 1. Redistributions of source code must retain the above copyright notice, this\n//    list of conditions and the following disclaimer.\n//\n// 2. Redistributions in binary form must reproduce the above copyright notice,\n//    this list of conditions and the following disclaimer in the documentation\n//    and/or other materials provided with the distribution.\n//\n// 3. Neither the name of the copyright holder nor the names of its\n//    contributors may be used to endorse or promote products derived from\n//    this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\n// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for Expat XML Parser\n--------------------\nCopyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper\nCopyright (c) 2001-2022 Expat maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n--------------------\nLicense notice for ffmpeg\n--------------------\n# License\n\nMost files in FFmpeg are under the GNU Lesser General Public License version 2.1\nor later (LGPL v2.1+). Read the file `COPYING.LGPLv2.1` for details. Some other\nfiles have MIT/X11/BSD-style licenses. In combination the LGPL v2.1+ applies to\nFFmpeg.\n\nSome optional parts of FFmpeg are licensed under the GNU General Public License\nversion 2 or later (GPL v2+). See the file `COPYING.GPLv2` for details. None of\nthese parts are used by default, you have to explicitly pass `--enable-gpl` to\nconfigure to activate them. In this case, FFmpeg's license changes to GPL v2+.\n\nSpecifically, the GPL parts of FFmpeg are:\n\n- libpostproc\n- optional x86 optimization in the files\n    - `libavcodec/x86/flac_dsp_gpl.asm`\n    - `libavcodec/x86/idct_mmx.c`\n    - `libavfilter/x86/vf_removegrain.asm`\n- the following building and testing tools\n    - `compat/solaris/make_sunver.pl`\n    - `doc/t2h.pm`\n    - `doc/texi2pod.pl`\n    - `libswresample/tests/swresample.c`\n    - `tests/checkasm/*`\n    - `tests/tiny_ssim.c`\n- the following filters in libavfilter:\n    - `signature_lookup.c`\n    - `vf_blackframe.c`\n    - `vf_boxblur.c`\n    - `vf_colormatrix.c`\n    - `vf_cover_rect.c`\n    - `vf_cropdetect.c`\n    - `vf_delogo.c`\n    - `vf_eq.c`\n    - `vf_find_rect.c`\n    - `vf_fspp.c`\n    - `vf_histeq.c`\n    - `vf_hqdn3d.c`\n    - `vf_kerndeint.c`\n    - `vf_lensfun.c` (GPL version 3 or later)\n    - `vf_mcdeint.c`\n    - `vf_mpdecimate.c`\n    - `vf_nnedi.c`\n    - `vf_owdenoise.c`\n    - `vf_perspective.c`\n    - `vf_phase.c`\n    - `vf_pp.c`\n    - `vf_pp7.c`\n    - `vf_pullup.c`\n    - `vf_repeatfields.c`\n    - `vf_sab.c`\n    - `vf_signature.c`\n    - `vf_smartblur.c`\n    - `vf_spp.c`\n    - `vf_stereo3d.c`\n    - `vf_super2xsai.c`\n    - `vf_tinterlace.c`\n    - `vf_uspp.c`\n    - `vf_vaguedenoiser.c`\n    - `vsrc_mptestsrc.c`\n\nShould you, for whatever reason, prefer to use version 3 of the (L)GPL, then\nthe configure parameter `--enable-version3` will activate this licensing option\nfor you. Read the file `COPYING.LGPLv3` or, if you have enabled GPL parts,\n`COPYING.GPLv3` to learn the exact legal terms that apply in this case.\n\nThere are a handful of files under other licensing terms, namely:\n\n* The files `libavcodec/jfdctfst.c`, `libavcodec/jfdctint_template.c` and\n  `libavcodec/jrevdct.c` are taken from libjpeg, see the top of the files for\n  licensing details. Specifically note that you must credit the IJG in the\n  documentation accompanying your program if you only distribute executables.\n  You must also indicate any changes including additions and deletions to\n  those three files in the documentation.\n* `tests/reference.pnm` is under the expat license.\n\n\n## External libraries\n\nFFmpeg can be combined with a number of external libraries, which sometimes\naffect the licensing of binaries resulting from the combination.\n\n### Compatible libraries\n\nThe following libraries are under GPL version 2:\n- avisynth\n- frei0r\n- libcdio\n- libdavs2\n- librubberband\n- libvidstab\n- libx264\n- libx265\n- libxavs\n- libxavs2\n- libxvid\n\nWhen combining them with FFmpeg, FFmpeg needs to be licensed as GPL as well by\npassing `--enable-gpl` to configure.\n\nThe following libraries are under LGPL version 3:\n- gmp\n- libaribb24\n- liblensfun\n\nWhen combining them with FFmpeg, use the configure option `--enable-version3` to\nupgrade FFmpeg to the LGPL v3.\n\nThe VMAF, mbedTLS, RK MPI, OpenCORE and VisualOn libraries are under the Apache License\n2.0. That license is incompatible with the LGPL v2.1 and the GPL v2, but not with\nversion 3 of those licenses. So to combine these libraries with FFmpeg, the\nlicense version needs to be upgraded by passing `--enable-version3` to configure.\n\nThe smbclient library is under the GPL v3, to combine it with FFmpeg,\nthe options `--enable-gpl` and `--enable-version3` have to be passed to\nconfigure to upgrade FFmpeg to the GPL v3.\n\n### Incompatible libraries\n\nThere are certain libraries you can combine with FFmpeg whose licenses are not\ncompatible with the GPL and/or the LGPL. If you wish to enable these\nlibraries, even in circumstances that their license may be incompatible, pass\n`--enable-nonfree` to configure. This will cause the resulting binary to be\nunredistributable.\n\nThe Fraunhofer FDK AAC and OpenSSL libraries are under licenses which are\nincompatible with the GPLv2 and v3. To the best of our knowledge, they are\ncompatible with the LGPL.\n\n\n********************************************************************************\n\nlibavformat/oggparsetheora.c\n\nCopyright (C) 2005  Matthieu CASTET, Alex Beregszaszi\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use, copy,\nmodify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n********************************************************************************\n\nlibavutil/x86/x86inc.asm\n\nx86inc.asm: x86 abstraction layer\n\n Copyright (C) 2005-2024 x264 project\n\n Authors: Loren Merritt <lorenm@u.washington.edu>\n          Henrik Gramner <henrik@gramner.com>\n          Anton Mitrofanov <BugMaster@narod.ru>\n          Fiona Glaser <fiona@x264.com>\n\n Permission to use, copy, modify, and/or distribute this software for any\n purpose with or without fee is hereby granted, provided that the above\n copyright notice and this permission notice appear in all copies.\n\n THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\n********************************************************************************\n\nlibavcodec/mips/compute_antialias_fixed.h\nlibavcodec/mips/compute_antialias_float.h\nlibavutil/fixed_dsp.c\nlibavutil/fixed_dsp.h\nlibavutil/mips/libm_mips.h\nlibavutil/softfloat_tables.h\n\nCopyright (c) 2012\nMIPS Technologies, Inc., California.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n1. Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n3. Neither the name of the MIPS Technologies, Inc., nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE MIPS TECHNOLOGIES, INC. ``AS IS'' AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED.  IN NO EVENT SHALL THE MIPS TECHNOLOGIES, INC. BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\nOR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\nHOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\nLIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\nOUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGE.\n\nAuthors:\nBranimir Vasic   (bvasic@mips.com)\nDarko Laus       (darko@mips.com)\nDjordje Pesut    (djordje@mips.com)\nGoran Cordasic   (goran@mips.com)\nNedeljko Babic   (nedeljko.babic imgtec com)\nMirjana Vulin    (mvulin@mips.com)\nStanislav Ocovaj (socovaj@mips.com)\nZoran Lukic      (zoranl@mips.com)\n\n********************************************************************************\n\nlibavformat/oggdec.c\nlibavformat/oggdec.h\nlibavformat/oggparseogm.c\nlibavformat/oggparsevorbis.c\n\nCopyright (C) 2005  Michael Ahlberg, MÃ¥ns RullgÃ¥rd\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use, copy,\nmodify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n********************************************************************************\n\n                  GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\n                  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n\n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n                            NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n--------------------\nLicense notice for FreeType\n--------------------\n                    The FreeType Project LICENSE\n                    ----------------------------\n\n                            2006-Jan-27\n\n                    Copyright 1996-2002, 2006 by\n          David Turner, Robert Wilhelm, and Werner Lemberg\n\n\n\nIntroduction\n============\n\n  The FreeType  Project is distributed in  several archive packages;\n  some of them may contain, in addition to the FreeType font engine,\n  various tools and  contributions which rely on, or  relate to, the\n  FreeType Project.\n\n  This  license applies  to all  files found  in such  packages, and\n  which do not  fall under their own explicit  license.  The license\n  affects  thus  the  FreeType   font  engine,  the  test  programs,\n  documentation and makefiles, at the very least.\n\n  This  license   was  inspired  by  the  BSD,   Artistic,  and  IJG\n  (Independent JPEG  Group) licenses, which  all encourage inclusion\n  and  use of  free  software in  commercial  and freeware  products\n  alike.  As a consequence, its main points are that:\n\n    o We don't promise that this software works. However, we will be\n      interested in any kind of bug reports. (`as is' distribution)\n\n    o You can  use this software for whatever you  want, in parts or\n      full form, without having to pay us. (`royalty-free' usage)\n\n    o You may not pretend that  you wrote this software.  If you use\n      it, or  only parts of it,  in a program,  you must acknowledge\n      somewhere  in  your  documentation  that  you  have  used  the\n      FreeType code. (`credits')\n\n  We  specifically  permit  and  encourage  the  inclusion  of  this\n  software, with  or without modifications,  in commercial products.\n  We  disclaim  all warranties  covering  The  FreeType Project  and\n  assume no liability related to The FreeType Project.\n\n\n  Finally,  many  people  asked  us  for  a  preferred  form  for  a\n  credit/disclaimer to use in compliance with this license.  We thus\n  encourage you to use the following text:\n\n   \"\"\"\n    Portions of this software are copyright © <year> The FreeType\n    Project (www.freetype.org).  All rights reserved.\n   \"\"\"\n\n  Please replace <year> with the value from the FreeType version you\n  actually use.\n\n\nLegal Terms\n===========\n\n0. Definitions\n--------------\n\n  Throughout this license,  the terms `package', `FreeType Project',\n  and  `FreeType  archive' refer  to  the  set  of files  originally\n  distributed  by the  authors  (David Turner,  Robert Wilhelm,  and\n  Werner Lemberg) as the `FreeType Project', be they named as alpha,\n  beta or final release.\n\n  `You' refers to  the licensee, or person using  the project, where\n  `using' is a generic term including compiling the project's source\n  code as  well as linking it  to form a  `program' or `executable'.\n  This  program is  referred to  as  `a program  using the  FreeType\n  engine'.\n\n  This  license applies  to all  files distributed  in  the original\n  FreeType  Project,   including  all  source   code,  binaries  and\n  documentation,  unless  otherwise  stated   in  the  file  in  its\n  original, unmodified form as  distributed in the original archive.\n  If you are  unsure whether or not a particular  file is covered by\n  this license, you must contact us to verify this.\n\n  The FreeType  Project is copyright (C) 1996-2000  by David Turner,\n  Robert Wilhelm, and Werner Lemberg.  All rights reserved except as\n  specified below.\n\n1. No Warranty\n--------------\n\n  THE FREETYPE PROJECT  IS PROVIDED `AS IS' WITHOUT  WARRANTY OF ANY\n  KIND, EITHER  EXPRESS OR IMPLIED,  INCLUDING, BUT NOT  LIMITED TO,\n  WARRANTIES  OF  MERCHANTABILITY   AND  FITNESS  FOR  A  PARTICULAR\n  PURPOSE.  IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS\n  BE LIABLE  FOR ANY DAMAGES CAUSED  BY THE USE OR  THE INABILITY TO\n  USE, OF THE FREETYPE PROJECT.\n\n2. Redistribution\n-----------------\n\n  This  license  grants  a  worldwide, royalty-free,  perpetual  and\n  irrevocable right  and license to use,  execute, perform, compile,\n  display,  copy,   create  derivative  works   of,  distribute  and\n  sublicense the  FreeType Project (in  both source and  object code\n  forms)  and  derivative works  thereof  for  any  purpose; and  to\n  authorize others  to exercise  some or all  of the  rights granted\n  herein, subject to the following conditions:\n\n    o Redistribution of  source code  must retain this  license file\n      (`FTL.TXT') unaltered; any  additions, deletions or changes to\n      the original  files must be clearly  indicated in accompanying\n      documentation.   The  copyright   notices  of  the  unaltered,\n      original  files must  be  preserved in  all  copies of  source\n      files.\n\n    o Redistribution in binary form must provide a  disclaimer  that\n      states  that  the software is based in part of the work of the\n      FreeType Team,  in  the  distribution  documentation.  We also\n      encourage you to put an URL to the FreeType web page  in  your\n      documentation, though this isn't mandatory.\n\n  These conditions  apply to any  software derived from or  based on\n  the FreeType Project,  not just the unmodified files.   If you use\n  our work, you  must acknowledge us.  However, no  fee need be paid\n  to us.\n\n3. Advertising\n--------------\n\n  Neither the  FreeType authors and  contributors nor you  shall use\n  the name of the  other for commercial, advertising, or promotional\n  purposes without specific prior written permission.\n\n  We suggest,  but do not require, that  you use one or  more of the\n  following phrases to refer  to this software in your documentation\n  or advertising  materials: `FreeType Project',  `FreeType Engine',\n  `FreeType library', or `FreeType Distribution'.\n\n  As  you have  not signed  this license,  you are  not  required to\n  accept  it.   However,  as  the FreeType  Project  is  copyrighted\n  material, only  this license, or  another one contracted  with the\n  authors, grants you  the right to use, distribute,  and modify it.\n  Therefore,  by  using,  distributing,  or modifying  the  FreeType\n  Project, you indicate that you understand and accept all the terms\n  of this license.\n\n4. Contacts\n-----------\n\n  There are two mailing lists related to FreeType:\n\n    o freetype@nongnu.org\n\n      Discusses general use and applications of FreeType, as well as\n      future and  wanted additions to the  library and distribution.\n      If  you are looking  for support,  start in  this list  if you\n      haven't found anything to help you in the documentation.\n\n    o freetype-devel@nongnu.org\n\n      Discusses bugs,  as well  as engine internals,  design issues,\n      specific licenses, porting, etc.\n\n  Our home page can be found at\n\n    https://www.freetype.org\n\n\n--- end of FTL.TXT ---\n\n--------------------\nLicense notice for harfbuzz-ng\n--------------------\nHarfBuzz is licensed under the so-called \"Old MIT\" license.  Details follow.\nFor parts of HarfBuzz that are licensed under different licenses see individual\nfiles names COPYING in subdirectories where applicable.\n\nCopyright © 2010-2022  Google, Inc.\nCopyright © 2015-2020  Ebrahim Byagowi\nCopyright © 2019,2020  Facebook, Inc.\nCopyright © 2012,2015  Mozilla Foundation\nCopyright © 2011  Codethink Limited\nCopyright © 2008,2010  Nokia Corporation and/or its subsidiary(-ies)\nCopyright © 2009  Keith Stribley\nCopyright © 2011  Martin Hosken and SIL International\nCopyright © 2007  Chris Wilson\nCopyright © 2005,2006,2020,2021,2022,2023  Behdad Esfahbod\nCopyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023  Red Hat, Inc.\nCopyright © 1998-2005  David Turner and Werner Lemberg\nCopyright © 2016  Igalia S.L.\nCopyright © 2022  Matthias Clasen\nCopyright © 2018,2021  Khaled Hosny\nCopyright © 2018,2019,2020  Adobe, Inc\nCopyright © 2013-2015  Alexei Podtelezhnikov\n\nFor full copyright notices consult the individual files in the package.\n\n\nPermission is hereby granted, without written agreement and without\nlicense or royalty fees, to use, copy, modify, and distribute this\nsoftware and its documentation for any purpose, provided that the\nabove copyright notice and the following two paragraphs appear in\nall copies of this software.\n\nIN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR\nDIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES\nARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN\nIF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGE.\n\nTHE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,\nBUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS\nON AN \"AS IS\" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO\nPROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.\n\n--------------------\nLicense notice for icu\n--------------------\nUNICODE LICENSE V3\n\nCOPYRIGHT AND PERMISSION NOTICE\n\nCopyright © 2016-2023 Unicode, Inc.\n\nNOTICE TO USER: Carefully read the following legal agreement. BY\nDOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR\nSOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE\nTERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT\nDOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of data files and any associated documentation (the \"Data Files\") or\nsoftware and any associated documentation (the \"Software\") to deal in the\nData Files or Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, and/or sell\ncopies of the Data Files or Software, and to permit persons to whom the\nData Files or Software are furnished to do so, provided that either (a)\nthis copyright and permission notice appear with all copies of the Data\nFiles or Software, or (b) this copyright and permission notice appear in\nassociated Documentation.\n\nTHE DATA FILES AND SOFTWARE ARE PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY\nKIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF\nTHIRD PARTY RIGHTS.\n\nIN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE\nBE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,\nOR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,\nWHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,\nARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA\nFILES OR SOFTWARE.\n\nExcept as contained in this notice, the name of a copyright holder shall\nnot be used in advertising or otherwise to promote the sale, use or other\ndealings in these Data Files or Software without prior written\nauthorization of the copyright holder.\n\n----------------------------------------------------------------------\n\nThird-Party Software Licenses\n\nThis section contains third-party software notices and/or additional\nterms for licensed third-party software components included within ICU\nlibraries.\n\n----------------------------------------------------------------------\n\nICU License - ICU 1.8.1 to ICU 57.1\n\nCOPYRIGHT AND PERMISSION NOTICE\n\nCopyright (c) 1995-2016 International Business Machines Corporation and others\nAll rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, and/or sell copies of the Software, and to permit persons\nto whom the Software is furnished to do so, provided that the above\ncopyright notice(s) and this permission notice appear in all copies of\nthe Software and that both the above copyright notice(s) and this\npermission notice appear in supporting documentation.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\nHOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY\nSPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER\nRESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF\nCONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN\nCONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\nExcept as contained in this notice, the name of a copyright holder\nshall not be used in advertising or otherwise to promote the sale, use\nor other dealings in this Software without prior written authorization\nof the copyright holder.\n\nAll trademarks and registered trademarks mentioned herein are the\nproperty of their respective owners.\n\n----------------------------------------------------------------------\n\nChinese/Japanese Word Break Dictionary Data (cjdict.txt)\n\n #     The Google Chrome software developed by Google is licensed under\n # the BSD license. Other software included in this distribution is\n # provided under other licenses, as set forth below.\n #\n #  The BSD License\n #  http://opensource.org/licenses/bsd-license.php\n #  Copyright (C) 2006-2008, Google Inc.\n #\n #  All rights reserved.\n #\n #  Redistribution and use in source and binary forms, with or without\n # modification, are permitted provided that the following conditions are met:\n #\n #  Redistributions of source code must retain the above copyright notice,\n # this list of conditions and the following disclaimer.\n #  Redistributions in binary form must reproduce the above\n # copyright notice, this list of conditions and the following\n # disclaimer in the documentation and/or other materials provided with\n # the distribution.\n #  Neither the name of  Google Inc. nor the names of its\n # contributors may be used to endorse or promote products derived from\n # this software without specific prior written permission.\n #\n #\n #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\n # CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\n # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR\n # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\n # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n #\n #\n #  The word list in cjdict.txt are generated by combining three word lists\n # listed below with further processing for compound word breaking. The\n # frequency is generated with an iterative training against Google web\n # corpora.\n #\n #  * Libtabe (Chinese)\n #    - https://sourceforge.net/project/?group_id=1519\n #    - Its license terms and conditions are shown below.\n #\n #  * IPADIC (Japanese)\n #    - http://chasen.aist-nara.ac.jp/chasen/distribution.html\n #    - Its license terms and conditions are shown below.\n #\n #  ---------COPYING.libtabe ---- BEGIN--------------------\n #\n #  /*\n #   * Copyright (c) 1999 TaBE Project.\n #   * Copyright (c) 1999 Pai-Hsiang Hsiao.\n #   * All rights reserved.\n #   *\n #   * Redistribution and use in source and binary forms, with or without\n #   * modification, are permitted provided that the following conditions\n #   * are met:\n #   *\n #   * . Redistributions of source code must retain the above copyright\n #   *   notice, this list of conditions and the following disclaimer.\n #   * . Redistributions in binary form must reproduce the above copyright\n #   *   notice, this list of conditions and the following disclaimer in\n #   *   the documentation and/or other materials provided with the\n #   *   distribution.\n #   * . Neither the name of the TaBE Project nor the names of its\n #   *   contributors may be used to endorse or promote products derived\n #   *   from this software without specific prior written permission.\n #   *\n #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n #   * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\n #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\n #   * OF THE POSSIBILITY OF SUCH DAMAGE.\n #   */\n #\n #  /*\n #   * Copyright (c) 1999 Computer Systems and Communication Lab,\n #   *                    Institute of Information Science, Academia\n #       *                    Sinica. All rights reserved.\n #   *\n #   * Redistribution and use in source and binary forms, with or without\n #   * modification, are permitted provided that the following conditions\n #   * are met:\n #   *\n #   * . Redistributions of source code must retain the above copyright\n #   *   notice, this list of conditions and the following disclaimer.\n #   * . Redistributions in binary form must reproduce the above copyright\n #   *   notice, this list of conditions and the following disclaimer in\n #   *   the documentation and/or other materials provided with the\n #   *   distribution.\n #   * . Neither the name of the Computer Systems and Communication Lab\n #   *   nor the names of its contributors may be used to endorse or\n #   *   promote products derived from this software without specific\n #   *   prior written permission.\n #   *\n #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n #   * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\n #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\n #   * OF THE POSSIBILITY OF SUCH DAMAGE.\n #   */\n #\n #  Copyright 1996 Chih-Hao Tsai @ Beckman Institute,\n #      University of Illinois\n #  c-tsai4@uiuc.edu  http://casper.beckman.uiuc.edu/~c-tsai4\n #\n #  ---------------COPYING.libtabe-----END--------------------------------\n #\n #\n #  ---------------COPYING.ipadic-----BEGIN-------------------------------\n #\n #  Copyright 2000, 2001, 2002, 2003 Nara Institute of Science\n #  and Technology.  All Rights Reserved.\n #\n #  Use, reproduction, and distribution of this software is permitted.\n #  Any copy of this software, whether in its original form or modified,\n #  must include both the above copyright notice and the following\n #  paragraphs.\n #\n #  Nara Institute of Science and Technology (NAIST),\n #  the copyright holders, disclaims all warranties with regard to this\n #  software, including all implied warranties of merchantability and\n #  fitness, in no event shall NAIST be liable for\n #  any special, indirect or consequential damages or any damages\n #  whatsoever resulting from loss of use, data or profits, whether in an\n #  action of contract, negligence or other tortuous action, arising out\n #  of or in connection with the use or performance of this software.\n #\n #  A large portion of the dictionary entries\n #  originate from ICOT Free Software.  The following conditions for ICOT\n #  Free Software applies to the current dictionary as well.\n #\n #  Each User may also freely distribute the Program, whether in its\n #  original form or modified, to any third party or parties, PROVIDED\n #  that the provisions of Section 3 (\"NO WARRANTY\") will ALWAYS appear\n #  on, or be attached to, the Program, which is distributed substantially\n #  in the same form as set out herein and that such intended\n #  distribution, if actually made, will neither violate or otherwise\n #  contravene any of the laws and regulations of the countries having\n #  jurisdiction over the User or the intended distribution itself.\n #\n #  NO WARRANTY\n #\n #  The program was produced on an experimental basis in the course of the\n #  research and development conducted during the project and is provided\n #  to users as so produced on an experimental basis.  Accordingly, the\n #  program is provided without any warranty whatsoever, whether express,\n #  implied, statutory or otherwise.  The term \"warranty\" used herein\n #  includes, but is not limited to, any warranty of the quality,\n #  performance, merchantability and fitness for a particular purpose of\n #  the program and the nonexistence of any infringement or violation of\n #  any right of any third party.\n #\n #  Each user of the program will agree and understand, and be deemed to\n #  have agreed and understood, that there is no warranty whatsoever for\n #  the program and, accordingly, the entire risk arising from or\n #  otherwise connected with the program is assumed by the user.\n #\n #  Therefore, neither ICOT, the copyright holder, or any other\n #  organization that participated in or was otherwise related to the\n #  development of the program and their respective officials, directors,\n #  officers and other employees shall be held liable for any and all\n #  damages, including, without limitation, general, special, incidental\n #  and consequential damages, arising out of or otherwise in connection\n #  with the use or inability to use the program or any product, material\n #  or result produced or otherwise obtained by using the program,\n #  regardless of whether they have been advised of, or otherwise had\n #  knowledge of, the possibility of such damages at any time during the\n #  project or thereafter.  Each user will be deemed to have agreed to the\n #  foregoing by his or her commencement of use of the program.  The term\n #  \"use\" as used herein includes, but is not limited to, the use,\n #  modification, copying and distribution of the program and the\n #  production of secondary products from the program.\n #\n #  In the case where the program, whether in its original form or\n #  modified, was distributed or delivered to or received by a user from\n #  any person, organization or entity other than ICOT, unless it makes or\n #  grants independently of ICOT any specific warranty to the user in\n #  writing, such person, organization or entity, will also be exempted\n #  from and not be held liable to the user for any such damages as noted\n #  above as far as the program is concerned.\n #\n #  ---------------COPYING.ipadic-----END----------------------------------\n\n----------------------------------------------------------------------\n\nLao Word Break Dictionary Data (laodict.txt)\n\n # Copyright (C) 2016 and later: Unicode, Inc. and others.\n # License & terms of use: http://www.unicode.org/copyright.html\n # Copyright (c) 2015 International Business Machines Corporation\n # and others. All Rights Reserved.\n #\n # Project: https://github.com/rober42539/lao-dictionary\n # Dictionary: https://github.com/rober42539/lao-dictionary/laodict.txt\n # License: https://github.com/rober42539/lao-dictionary/LICENSE.txt\n #          (copied below)\n #\n #\tThis file is derived from the above dictionary version of Nov 22, 2020\n #  ----------------------------------------------------------------------\n #  Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell.\n #  All rights reserved.\n #\n #  Redistribution and use in source and binary forms, with or without\n #  modification, are permitted provided that the following conditions are met:\n #\n #  Redistributions of source code must retain the above copyright notice, this\n #  list of conditions and the following disclaimer. Redistributions in binary\n #  form must reproduce the above copyright notice, this list of conditions and\n #  the following disclaimer in the documentation and/or other materials\n #  provided with the distribution.\n #\n # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n # \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,\n # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\n # OF THE POSSIBILITY OF SUCH DAMAGE.\n #  --------------------------------------------------------------------------\n\n----------------------------------------------------------------------\n\nBurmese Word Break Dictionary Data (burmesedict.txt)\n\n #  Copyright (c) 2014 International Business Machines Corporation\n #  and others. All Rights Reserved.\n #\n #  This list is part of a project hosted at:\n #    github.com/kanyawtech/myanmar-karen-word-lists\n #\n #  --------------------------------------------------------------------------\n #  Copyright (c) 2013, LeRoy Benjamin Sharon\n #  All rights reserved.\n #\n #  Redistribution and use in source and binary forms, with or without\n #  modification, are permitted provided that the following conditions\n #  are met: Redistributions of source code must retain the above\n #  copyright notice, this list of conditions and the following\n #  disclaimer.  Redistributions in binary form must reproduce the\n #  above copyright notice, this list of conditions and the following\n #  disclaimer in the documentation and/or other materials provided\n #  with the distribution.\n #\n #    Neither the name Myanmar Karen Word Lists, nor the names of its\n #    contributors may be used to endorse or promote products derived\n #    from this software without specific prior written permission.\n #\n #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\n #  CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n #  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\n #  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS\n #  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n #  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\n #  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n #  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n #  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR\n #  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF\n #  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\n #  SUCH DAMAGE.\n #  --------------------------------------------------------------------------\n\n----------------------------------------------------------------------\n\nTime Zone Database\n\n  ICU uses the public domain data and code derived from Time Zone\nDatabase for its time zone support. The ownership of the TZ database\nis explained in BCP 175: Procedure for Maintaining the Time Zone\nDatabase section 7.\n\n # 7.  Database Ownership\n #\n #    The TZ database itself is not an IETF Contribution or an IETF\n #    document.  Rather it is a pre-existing and regularly updated work\n #    that is in the public domain, and is intended to remain in the\n #    public domain.  Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do\n #    not apply to the TZ Database or contributions that individuals make\n #    to it.  Should any claims be made and substantiated against the TZ\n #    Database, the organization that is providing the IANA\n #    Considerations defined in this RFC, under the memorandum of\n #    understanding with the IETF, currently ICANN, may act in accordance\n #    with all competent court orders.  No ownership claims will be made\n #    by ICANN or the IETF Trust on the database or the code.  Any person\n #    making a contribution to the database or code waives all rights to\n #    future claims in that contribution or in the TZ Database.\n\n----------------------------------------------------------------------\n\nGoogle double-conversion\n\nCopyright 2006-2011, the V8 project authors. All rights reserved.\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\n      copyright notice, this list of conditions and the following\n      disclaimer in the documentation and/or other materials provided\n      with the distribution.\n    * Neither the name of Google Inc. nor the names of its\n      contributors may be used to endorse or promote products derived\n      from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n----------------------------------------------------------------------\n\nFile: aclocal.m4 (only for ICU4C)\nSection: pkg.m4 - Macros to locate and utilise pkg-config.\n\n\nCopyright © 2004 Scott James Remnant <scott@netsplit.com>.\nCopyright © 2012-2015 Dan Nicholson <dbn.lists@gmail.com>\n\nThis program is free software; you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation; either version 2 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but\nWITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\nGeneral Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program; if not, write to the Free Software\nFoundation, Inc., 59 Temple Place - Suite 330, Boston, MA\n02111-1307, USA.\n\nAs a special exception to the GNU General Public License, if you\ndistribute this file as part of a program that contains a\nconfiguration script generated by Autoconf, you may include it under\nthe same distribution terms that you use for the rest of that\nprogram.\n\n\n(The condition for the exception is fulfilled because\nICU4C includes a configuration script generated by Autoconf,\nnamely the `configure` script.)\n\n----------------------------------------------------------------------\n\nFile: config.guess (only for ICU4C)\n\n\nThis file is free software; you can redistribute it and/or modify it\nunder the terms of the GNU General Public License as published by\nthe Free Software Foundation; either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but\nWITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\nGeneral Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program; if not, see <https://www.gnu.org/licenses/>.\n\nAs a special exception to the GNU General Public License, if you\ndistribute this file as part of a program that contains a\nconfiguration script generated by Autoconf, you may include it under\nthe same distribution terms that you use for the rest of that\nprogram.  This Exception is an additional permission under section 7\nof the GNU General Public License, version 3 (\"GPLv3\").\n\n\n(The condition for the exception is fulfilled because\nICU4C includes a configuration script generated by Autoconf,\nnamely the `configure` script.)\n\n----------------------------------------------------------------------\n\nFile: install-sh (only for ICU4C)\n\n\nCopyright 1991 by the Massachusetts Institute of Technology\n\nPermission to use, copy, modify, distribute, and sell this software and its\ndocumentation for any purpose is hereby granted without fee, provided that\nthe above copyright notice appear in all copies and that both that\ncopyright notice and this permission notice appear in supporting\ndocumentation, and that the name of M.I.T. not be used in advertising or\npublicity pertaining to distribution of the software without specific,\nwritten prior permission.  M.I.T. makes no representations about the\nsuitability of this software for any purpose.  It is provided \"as is\"\nwithout express or implied warranty.\n\n--------------------\nLicense notice for ipcz\n--------------------\n// Copyright 2022 The Chromium Authors\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n//    * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n//    * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n//    * Neither the name of Google LLC nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for jsoncpp\n--------------------\nThe JsonCpp library's source code, including accompanying documentation, \ntests and demonstration applications, are licensed under the following\nconditions...\n\nThe author (Baptiste Lepilleur) explicitly disclaims copyright in all \njurisdictions which recognize such a disclaimer. In such jurisdictions, \nthis software is released into the Public Domain.\n\nIn jurisdictions which do not recognize Public Domain property (e.g. Germany as of\n2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is\nreleased under the terms of the MIT License (see below).\n\nIn jurisdictions which recognize Public Domain property, the user of this \nsoftware may choose to accept it either as 1) Public Domain, 2) under the \nconditions of the MIT License (see below), or 3) under the terms of dual \nPublic Domain/MIT License conditions described here, as they choose.\n\nThe MIT License is about as close to Public Domain as a license can get, and is\ndescribed in clear, concise terms at:\n\n   http://en.wikipedia.org/wiki/MIT_License\n   \nThe full text of the MIT License follows:\n\n========================================================================\nCopyright (c) 2007-2010 Baptiste Lepilleur\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use, copy,\nmodify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\nBE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\nACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n========================================================================\n(END LICENSE TEXT)\n\nThe MIT license is compatible with both the GPL and commercial\nsoftware, affording one all of the rights of Public Domain with the\nminor nuisance of being required to keep the above copyright notice\nand license text in the source code. Note also that by accepting the\nPublic Domain \"license\" you can re-license your copy using whatever\nlicense you like.\n\n--------------------\nLicense notice for google-jstemplate\n--------------------\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------\nLicense notice for Alliance for Open Media Video Codec\n--------------------\nCopyright (c) 2016, Alliance for Open Media. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in\n   the documentation and/or other materials provided with the\n   distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\nFOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\nINCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\nBUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\nLIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\nANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n\n\n--------------------\nLicense notice for libjpeg-turbo\n--------------------\nlibjpeg-turbo Licenses\n======================\n\nlibjpeg-turbo is covered by three compatible BSD-style open source licenses:\n\n- The IJG (Independent JPEG Group) License, which is listed in\n  [README.ijg](README.ijg)\n\n  This license applies to the libjpeg API library and associated programs\n  (any code inherited from libjpeg, and any modifications to that code.)\n\n- The Modified (3-clause) BSD License, which is listed below\n\n  This license covers the TurboJPEG API library and associated programs, as\n  well as the build system.\n\n- The [zlib License](https://opensource.org/licenses/Zlib)\n\n  This license is a subset of the other two, and it covers the libjpeg-turbo\n  SIMD extensions.\n\n\nComplying with the libjpeg-turbo Licenses\n=========================================\n\nThis section provides a roll-up of the libjpeg-turbo licensing terms, to the\nbest of our understanding.\n\n1.  If you are distributing a modified version of the libjpeg-turbo source,\n    then:\n\n    1.  You cannot alter or remove any existing copyright or license notices\n        from the source.\n\n        **Origin**\n        - Clause 1 of the IJG License\n        - Clause 1 of the Modified BSD License\n        - Clauses 1 and 3 of the zlib License\n\n    2.  You must add your own copyright notice to the header of each source\n        file you modified, so others can tell that you modified that file (if\n        there is not an existing copyright header in that file, then you can\n        simply add a notice stating that you modified the file.)\n\n        **Origin**\n        - Clause 1 of the IJG License\n        - Clause 2 of the zlib License\n\n    3.  You must include the IJG README file, and you must not alter any of the\n        copyright or license text in that file.\n\n        **Origin**\n        - Clause 1 of the IJG License\n\n2.  If you are distributing only libjpeg-turbo binaries without the source, or\n    if you are distributing an application that statically links with\n    libjpeg-turbo, then:\n\n    1.  Your product documentation must include a message stating:\n\n        This software is based in part on the work of the Independent JPEG\n        Group.\n\n        **Origin**\n        - Clause 2 of the IJG license\n\n    2.  If your binary distribution includes or uses the TurboJPEG API, then\n        your product documentation must include the text of the Modified BSD\n        License (see below.)\n\n        **Origin**\n        - Clause 2 of the Modified BSD License\n\n3.  You cannot use the name of the IJG or The libjpeg-turbo Project or the\n    contributors thereof in advertising, publicity, etc.\n\n    **Origin**\n    - IJG License\n    - Clause 3 of the Modified BSD License\n\n4.  The IJG and The libjpeg-turbo Project do not warrant libjpeg-turbo to be\n    free of defects, nor do we accept any liability for undesirable\n    consequences resulting from your use of the software.\n\n    **Origin**\n    - IJG License\n    - Modified BSD License\n    - zlib License\n\n\nThe Modified (3-clause) BSD License\n===================================\n\nCopyright (C)2009-2023 D. R. Commander.  All Rights Reserved.<br>\nCopyright (C)2015 Viktor Szathmáry.  All Rights Reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n- Redistributions of source code must retain the above copyright notice,\n  this list of conditions and the following disclaimer.\n- Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n- Neither the name of the libjpeg-turbo Project nor the names of its\n  contributors may be used to endorse or promote products derived from this\n  software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\",\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n\n\nWhy Three Licenses?\n===================\n\nThe zlib License could have been used instead of the Modified (3-clause) BSD\nLicense, and since the IJG License effectively subsumes the distribution\nconditions of the zlib License, this would have effectively placed\nlibjpeg-turbo binary distributions under the IJG License.  However, the IJG\nLicense specifically refers to the Independent JPEG Group and does not extend\nattribution and endorsement protections to other entities.  Thus, it was\ndesirable to choose a license that granted us the same protections for new code\nthat were granted to the IJG for code derived from their software.\n\n--------------------\nLicense notice for libpng\n--------------------\nCOPYRIGHT NOTICE, DISCLAIMER, and LICENSE\n=========================================\n\nPNG Reference Library License version 2\n---------------------------------------\n\n * Copyright (c) 1995-2019 The PNG Reference Library Authors.\n * Copyright (c) 2018-2019 Cosmin Truta.\n * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson.\n * Copyright (c) 1996-1997 Andreas Dilger.\n * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.\n\nThe software is supplied \"as is\", without warranty of any kind,\nexpress or implied, including, without limitation, the warranties\nof merchantability, fitness for a particular purpose, title, and\nnon-infringement.  In no event shall the Copyright owners, or\nanyone distributing the software, be liable for any damages or\nother liability, whether in contract, tort or otherwise, arising\nfrom, out of, or in connection with the software, or the use or\nother dealings in the software, even if advised of the possibility\nof such damage.\n\nPermission is hereby granted to use, copy, modify, and distribute\nthis software, or portions hereof, for any purpose, without fee,\nsubject to the following restrictions:\n\n 1. The origin of this software must not be misrepresented; you\n    must not claim that you wrote the original software.  If you\n    use this software in a product, an acknowledgment in the product\n    documentation would be appreciated, but is not required.\n\n 2. Altered source versions must be plainly marked as such, and must\n    not be misrepresented as being the original software.\n\n 3. This Copyright notice may not be removed or altered from any\n    source or altered source distribution.\n\n\nPNG Reference Library License version 1 (for libpng 0.5 through 1.6.35)\n-----------------------------------------------------------------------\n\nlibpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are\nCopyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are\nderived from libpng-1.0.6, and are distributed according to the same\ndisclaimer and license as libpng-1.0.6 with the following individuals\nadded to the list of Contributing Authors:\n\n    Simon-Pierre Cadieux\n    Eric S. Raymond\n    Mans Rullgard\n    Cosmin Truta\n    Gilles Vollant\n    James Yu\n    Mandar Sahastrabuddhe\n    Google Inc.\n    Vadim Barkov\n\nand with the following additions to the disclaimer:\n\n    There is no warranty against interference with your enjoyment of\n    the library or against infringement.  There is no warranty that our\n    efforts or the library will fulfill any of your particular purposes\n    or needs.  This library is provided with all faults, and the entire\n    risk of satisfactory quality, performance, accuracy, and effort is\n    with the user.\n\nSome files in the \"contrib\" directory and some configure-generated\nfiles that are distributed with libpng have other copyright owners, and\nare released under other open source licenses.\n\nlibpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are\nCopyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from\nlibpng-0.96, and are distributed according to the same disclaimer and\nlicense as libpng-0.96, with the following individuals added to the\nlist of Contributing Authors:\n\n    Tom Lane\n    Glenn Randers-Pehrson\n    Willem van Schaik\n\nlibpng versions 0.89, June 1996, through 0.96, May 1997, are\nCopyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88,\nand are distributed according to the same disclaimer and license as\nlibpng-0.88, with the following individuals added to the list of\nContributing Authors:\n\n    John Bowler\n    Kevin Bracey\n    Sam Bushell\n    Magnus Holmgren\n    Greg Roelofs\n    Tom Tanner\n\nSome files in the \"scripts\" directory have other copyright owners,\nbut are released under this license.\n\nlibpng versions 0.5, May 1995, through 0.88, January 1996, are\nCopyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc.\n\nFor the purposes of this copyright and license, \"Contributing Authors\"\nis defined as the following set of individuals:\n\n    Andreas Dilger\n    Dave Martindale\n    Guy Eric Schalnat\n    Paul Schmidt\n    Tim Wegner\n\nThe PNG Reference Library is supplied \"AS IS\".  The Contributing\nAuthors and Group 42, Inc. disclaim all warranties, expressed or\nimplied, including, without limitation, the warranties of\nmerchantability and of fitness for any purpose.  The Contributing\nAuthors and Group 42, Inc. assume no liability for direct, indirect,\nincidental, special, exemplary, or consequential damages, which may\nresult from the use of the PNG Reference Library, even if advised of\nthe possibility of such damage.\n\nPermission is hereby granted to use, copy, modify, and distribute this\nsource code, or portions hereof, for any purpose, without fee, subject\nto the following restrictions:\n\n 1. The origin of this source code must not be misrepresented.\n\n 2. Altered versions must be plainly marked as such and must not\n    be misrepresented as being the original source.\n\n 3. This Copyright notice may not be removed or altered from any\n    source or altered source distribution.\n\nThe Contributing Authors and Group 42, Inc. specifically permit,\nwithout fee, and encourage the use of this source code as a component\nto supporting the PNG file format in commercial products.  If you use\nthis source code in a product, acknowledgment is not required but would\nbe appreciated.\n\n--------------------\nLicense notice for libsrtp\n--------------------\n/*\n *\t\n * Copyright (c) 2001-2017 Cisco Systems, Inc.\n * All rights reserved.\n * \n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * \n *   Redistributions of source code must retain the above copyright\n *   notice, this list of conditions and the following disclaimer.\n * \n *   Redistributions in binary form must reproduce the above\n *   copyright notice, this list of conditions and the following\n *   disclaimer in the documentation and/or other materials provided\n *   with the distribution.\n * \n *   Neither the name of the Cisco Systems, Inc. nor the names of its\n *   contributors may be used to endorse or promote products derived\n *   from this software without specific prior written permission.\n * \n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,\n * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\n * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED\n * OF THE POSSIBILITY OF SUCH DAMAGE.\n *\n */\n\n--------------------\nLicense notice for URL Pattern Library\n--------------------\nThe MIT License (MIT)\n\nCopyright 2020 The Chromium Authors\nCopyright (c) 2014 Blake Embrey (hello@blakeembrey.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n--------------------\nLicense notice for libvpx\n--------------------\nCopyright (c) 2010, The WebM Project authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n  * Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n\n  * Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in\n    the documentation and/or other materials provided with the\n    distribution.\n\n  * Neither the name of Google, nor the WebM Project, nor the names\n    of its contributors may be used to endorse or promote products\n    derived from this software without specific prior written\n    permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n--------------------\nLicense notice for WebP image encoder/decoder\n--------------------\nCopyright (c) 2010, Google Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n  * Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n\n  * Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in\n    the documentation and/or other materials provided with the\n    distribution.\n\n  * Neither the name of Google nor the names of its contributors may\n    be used to endorse or promote products derived from this software\n    without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nAdditional IP Rights Grant (Patents)\n------------------------------------\n\n\"These implementations\" means the copyrightable works that implement the WebM\ncodecs distributed by Google as part of the WebM Project.\n\nGoogle hereby grants to you a perpetual, worldwide, non-exclusive, no-charge,\nroyalty-free, irrevocable (except as stated in this section) patent license to\nmake, have made, use, offer to sell, sell, import, transfer, and otherwise\nrun, modify and propagate the contents of these implementations of WebM, where\nsuch license applies only to those patent claims, both currently owned by\nGoogle and acquired in the future, licensable by Google that are necessarily\ninfringed by these implementations of WebM. This grant does not include claims\nthat would be infringed only as a consequence of further modification of these\nimplementations. If you or your agent or exclusive licensee institute or order\nor agree to the institution of patent litigation or any other patent\nenforcement activity against any entity (including a cross-claim or\ncounterclaim in a lawsuit) alleging that any of these implementations of WebM\nor any code incorporated within any of these implementations of WebM\nconstitute direct or contributory patent infringement, or inducement of\npatent infringement, then any patent rights granted to you under this License\nfor these implementations of WebM shall terminate as of the date such\nlitigation is filed.\n\n--------------------\nLicense notice for libyuv\n--------------------\nCopyright 2011 The LibYuv Project Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n  * Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n\n  * Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in\n    the documentation and/or other materials provided with the\n    distribution.\n\n  * Neither the name of Google nor the names of its contributors may\n    be used to endorse or promote products derived from this software\n    without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for Lit\n--------------------\nBSD 3-Clause License\n\nCopyright (c) 2017 Google LLC. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n--------------------\nLicense notice for Lottie Web\n--------------------\nThe MIT License (MIT)\n\nCopyright (c) 2015 Bodymovin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n\n################################################################################\n# License headers for subpackages\n################################################################################\n\nTransformation Matrix v2.0\n(c) Epistemex 2014-2015\nwww.epistemex.com\nBy Ken Fyrstenberg\nContributions by leeoniya.\nLicense: MIT, header required.\n\n\n################################################################################\n\nCopyright 2014 David Bau.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n################################################################################\n\nBezierEasing - use bezier curve for transition easing function\nby Gaëtan Renaudeau 2014 - 2015 – MIT License\n\nCredits: is based on Firefox's nsSMILKeySpline.cpp\nUsage:\nvar spline = BezierEasing([ 0.25, 0.1, 0.25, 1.0 ])\nspline.get(x) => returns the easing value | x must be in [0, 1] range\n\n--------------------\nLicense notice for Metrics Protos\n--------------------\n// Copyright 2015 The Chromium Authors\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n//    * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n//    * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n//    * Neither the name of Google Inc. nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for modp base64 decoder\n--------------------\n * MODP_B64 - High performance base64 encoder/decoder\n * Version 1.3 -- 17-Mar-2006\n * http://modp.com/release/base64\n *\n * Copyright (c) 2005, 2006  Nick Galbreath -- nickg [at] modp [dot] com\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are\n * met:\n *\n *   Redistributions of source code must retain the above copyright\n *   notice, this list of conditions and the following disclaimer.\n *\n *   Redistributions in binary form must reproduce the above copyright\n *   notice, this list of conditions and the following disclaimer in the\n *   documentation and/or other materials provided with the distribution.\n *\n *   Neither the name of the modp.com nor the names of its\n *   contributors may be used to endorse or promote products derived from\n *   this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for OpenH264\n--------------------\nCopyright (c) 2013, Cisco Systems\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice, this\n  list of conditions and the following disclaimer in the documentation and/or\n  other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n--------------------\nLicense notice for opus\n--------------------\nCopyright 2001-2011 Xiph.Org, Skype Limited, Octasic,\n                    Jean-Marc Valin, Timothy B. Terriberry,\n                    CSIRO, Gregory Maxwell, Mark Borgerding,\n                    Erik de Castro Lopo\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n- Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\n- Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n\n- Neither the name of Internet Society, IETF or IETF Trust, nor the\nnames of specific contributors, may be used to endorse or promote\nproducts derived from this software without specific prior written\npermission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER\nOR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nOpus is subject to the royalty-free patent licenses which are\nspecified at:\n\nXiph.Org Foundation:\nhttps://datatracker.ietf.org/ipr/1524/\n\nMicrosoft Corporation:\nhttps://datatracker.ietf.org/ipr/1914/\n\nBroadcom Corporation:\nhttps://datatracker.ietf.org/ipr/1526/\n\n--------------------\nLicense notice for Perfetto\n--------------------\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   Copyright (c) 2017, The Android Open Source Project\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n\n   Copyright 2015 The Chromium Authors\n\n   Redistribution and use in source and binary forms, with or without\n   modification, are permitted provided that the following conditions are\n   met:\n\n      * Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n      * Redistributions in binary form must reproduce the above\n   copyright notice, this list of conditions and the following disclaimer\n   in the documentation and/or other materials provided with the\n   distribution.\n      * Neither the name of Google LLC nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\n   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n   \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n   OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n   OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for PFFFT: a pretty fast FFT.\n--------------------\nCopyright (c) 2013  Julien Pommier ( pommier@modartt.com )\n\nBased on original fortran 77 code from FFTPACKv4 from NETLIB,\nauthored by Dr Paul Swarztrauber of NCAR, in 1985.\n\nAs confirmed by the NCAR fftpack software curators, the following\nFFTPACKv5 license applies to FFTPACKv4 sources. My changes are\nreleased under the same terms.\n\nFFTPACK license:\n\nhttp://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html\n\nCopyright (c) 2004 the University Corporation for Atmospheric\nResearch (\"UCAR\"). All rights reserved. Developed by NCAR's\nComputational and Information Systems Laboratory, UCAR,\nwww.cisl.ucar.edu.\n\nRedistribution and use of the Software in source and binary forms,\nwith or without modification, is permitted provided that the\nfollowing conditions are met:\n\n- Neither the names of NCAR's Computational and Information Systems\nLaboratory, the University Corporation for Atmospheric Research,\nnor the names of its sponsors or contributors may be used to\nendorse or promote products derived from this Software without\nspecific prior written permission.\n\n- Redistributions of source code must retain the above copyright\nnotices, this list of conditions, and the disclaimer below.\n\n- Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions, and the disclaimer below in the\ndocumentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN\nACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE\nSOFTWARE.\n\n--------------------\nLicense notice for Polymer\n--------------------\n// Copyright (c) 2012 The Polymer Authors. All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n//    * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n//    * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n//    * Neither the name of Google Inc. nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for Protocol Buffers\n--------------------\nCopyright 2008 Google Inc.  All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n    * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n    * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nCode generated by the Protocol Buffer compiler is owned by the owner\nof the input file used when generating it.  This code is not\nstandalone and requires a support library to be linked with it.  This\nsupport library is itself covered by the above license.\n\n--------------------\nLicense notice for Protocol Buffers (javascript)\n--------------------\nBSD 3-Clause License\n\nCopyright (c) 2022, Google Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for re2 - an efficient, principled regular expression library\n--------------------\n// Copyright (c) 2009 The RE2 Authors. All rights reserved.\n//\n// Redistribution and use in source and binary forms, with or without\n// modification, are permitted provided that the following conditions are\n// met:\n//\n//    * Redistributions of source code must retain the above copyright\n// notice, this list of conditions and the following disclaimer.\n//    * Redistributions in binary form must reproduce the above\n// copyright notice, this list of conditions and the following disclaimer\n// in the documentation and/or other materials provided with the\n// distribution.\n//    * Neither the name of Google Inc. nor the names of its\n// contributors may be used to endorse or promote products derived from\n// this software without specific prior written permission.\n//\n// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n// \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for Recurrent neural network for audio noise reduction\n--------------------\nCopyright (c) 2017, Mozilla\nCopyright (c) 2007-2017, Jean-Marc Valin\nCopyright (c) 2005-2017, Xiph.Org Foundation\nCopyright (c) 2003-2004, Mark Borgerding\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n- Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\n- Redistributions in binary form must reproduce the above copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n\n- Neither the name of the Xiph.Org Foundation nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION\nOR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for bytemuck\n--------------------\nApache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n    1. Definitions.\n\n        \"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n\n        \"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n\n        \"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n        \"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n\n        \"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n\n        \"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\n\n        \"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\n\n        \"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\n\n        \"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"\n\n        \"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\n    2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\n    3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\n    4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\n        (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and\n        (b) You must cause any modified files to carry prominent notices stating that You changed the files; and\n        (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\n        (d) If the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.\n\n        You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\n    5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\n    6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\n    7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\n    8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\n    9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\nTo apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets \"[]\" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \"printed page\" as the copyright notice for easier identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n--------------------\nLicense notice for bytemuck_derive\n--------------------\nApache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n    1. Definitions.\n\n        \"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n\n        \"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n\n        \"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n        \"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n\n        \"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n\n        \"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\n\n        \"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\n\n        \"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\n\n        \"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"\n\n        \"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\n    2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\n    3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\n    4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\n        (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and\n        (b) You must cause any modified files to carry prominent notices stating that You changed the files; and\n        (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\n        (d) If the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.\n\n        You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\n    5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\n    6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\n    7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\n    8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\n    9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\nTo apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets \"[]\" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \"printed page\" as the copyright notice for easier identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n--------------------\nLicense notice for cxx\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for cxxbridge-macro\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for font-types\n--------------------\nApache License\n\nVersion 2.0, January 2004\n\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of this License; and\nYou must cause any modified files to carry prominent notices stating that You changed the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. \n\nYou may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\n5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nCopyright 2019 Colin Rothfels\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n--------------------\nLicense notice for itoa\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for proc-macro2\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for quote\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for read-fonts\n--------------------\nApache License\n\nVersion 2.0, January 2004\n\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of this License; and\nYou must cause any modified files to carry prominent notices stating that You changed the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. \n\nYou may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\n5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nCopyright 2019 Colin Rothfels\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n--------------------\nLicense notice for ryu\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for serde\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for serde_derive\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for serde_json_lenient\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for skrifa\n--------------------\nApache License\n\nVersion 2.0, January 2004\n\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \"control\" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising permissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \"submitted\" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of this License; and\nYou must cause any modified files to carry prominent notices stating that You changed the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. \n\nYou may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.\n5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nCopyright 2019 Colin Rothfels\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n--------------------\nLicense notice for syn\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for unicode-ident\n--------------------\n                              Apache License\n                        Version 2.0, January 2004\n                     http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n   \"License\" shall mean the terms and conditions for use, reproduction,\n   and distribution as defined by Sections 1 through 9 of this document.\n\n   \"Licensor\" shall mean the copyright owner or entity authorized by\n   the copyright owner that is granting the License.\n\n   \"Legal Entity\" shall mean the union of the acting entity and all\n   other entities that control, are controlled by, or are under common\n   control with that entity. For the purposes of this definition,\n   \"control\" means (i) the power, direct or indirect, to cause the\n   direction or management of such entity, whether by contract or\n   otherwise, or (ii) ownership of fifty percent (50%) or more of the\n   outstanding shares, or (iii) beneficial ownership of such entity.\n\n   \"You\" (or \"Your\") shall mean an individual or Legal Entity\n   exercising permissions granted by this License.\n\n   \"Source\" form shall mean the preferred form for making modifications,\n   including but not limited to software source code, documentation\n   source, and configuration files.\n\n   \"Object\" form shall mean any form resulting from mechanical\n   transformation or translation of a Source form, including but\n   not limited to compiled object code, generated documentation,\n   and conversions to other media types.\n\n   \"Work\" shall mean the work of authorship, whether in Source or\n   Object form, made available under the License, as indicated by a\n   copyright notice that is included in or attached to the work\n   (an example is provided in the Appendix below).\n\n   \"Derivative Works\" shall mean any work, whether in Source or Object\n   form, that is based on (or derived from) the Work and for which the\n   editorial revisions, annotations, elaborations, or other modifications\n   represent, as a whole, an original work of authorship. For the purposes\n   of this License, Derivative Works shall not include works that remain\n   separable from, or merely link (or bind by name) to the interfaces of,\n   the Work and Derivative Works thereof.\n\n   \"Contribution\" shall mean any work of authorship, including\n   the original version of the Work and any modifications or additions\n   to that Work or Derivative Works thereof, that is intentionally\n   submitted to Licensor for inclusion in the Work by the copyright owner\n   or by an individual or Legal Entity authorized to submit on behalf of\n   the copyright owner. For the purposes of this definition, \"submitted\"\n   means any form of electronic, verbal, or written communication sent\n   to the Licensor or its representatives, including but not limited to\n   communication on electronic mailing lists, source code control systems,\n   and issue tracking systems that are managed by, or on behalf of, the\n   Licensor for the purpose of discussing and improving the Work, but\n   excluding communication that is conspicuously marked or otherwise\n   designated in writing by the copyright owner as \"Not a Contribution.\"\n\n   \"Contributor\" shall mean Licensor and any individual or Legal Entity\n   on behalf of whom a Contribution has been received by Licensor and\n   subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   copyright license to reproduce, prepare Derivative Works of,\n   publicly display, publicly perform, sublicense, and distribute the\n   Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n   this License, each Contributor hereby grants to You a perpetual,\n   worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n   (except as stated in this section) patent license to make, have made,\n   use, offer to sell, sell, import, and otherwise transfer the Work,\n   where such license applies only to those patent claims licensable\n   by such Contributor that are necessarily infringed by their\n   Contribution(s) alone or by combination of their Contribution(s)\n   with the Work to which such Contribution(s) was submitted. If You\n   institute patent litigation against any entity (including a\n   cross-claim or counterclaim in a lawsuit) alleging that the Work\n   or a Contribution incorporated within the Work constitutes direct\n   or contributory patent infringement, then any patent licenses\n   granted to You under this License for that Work shall terminate\n   as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n   Work or Derivative Works thereof in any medium, with or without\n   modifications, and in Source or Object form, provided that You\n   meet the following conditions:\n\n   (a) You must give any other recipients of the Work or\n       Derivative Works a copy of this License; and\n\n   (b) You must cause any modified files to carry prominent notices\n       stating that You changed the files; and\n\n   (c) You must retain, in the Source form of any Derivative Works\n       that You distribute, all copyright, patent, trademark, and\n       attribution notices from the Source form of the Work,\n       excluding those notices that do not pertain to any part of\n       the Derivative Works; and\n\n   (d) If the Work includes a \"NOTICE\" text file as part of its\n       distribution, then any Derivative Works that You distribute must\n       include a readable copy of the attribution notices contained\n       within such NOTICE file, excluding those notices that do not\n       pertain to any part of the Derivative Works, in at least one\n       of the following places: within a NOTICE text file distributed\n       as part of the Derivative Works; within the Source form or\n       documentation, if provided along with the Derivative Works; or,\n       within a display generated by the Derivative Works, if and\n       wherever such third-party notices normally appear. The contents\n       of the NOTICE file are for informational purposes only and\n       do not modify the License. You may add Your own attribution\n       notices within Derivative Works that You distribute, alongside\n       or as an addendum to the NOTICE text from the Work, provided\n       that such additional attribution notices cannot be construed\n       as modifying the License.\n\n   You may add Your own copyright statement to Your modifications and\n   may provide additional or different license terms and conditions\n   for use, reproduction, or distribution of Your modifications, or\n   for any such Derivative Works as a whole, provided Your use,\n   reproduction, and distribution of the Work otherwise complies with\n   the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n   any Contribution intentionally submitted for inclusion in the Work\n   by You to the Licensor shall be under the terms and conditions of\n   this License, without any additional terms or conditions.\n   Notwithstanding the above, nothing herein shall supersede or modify\n   the terms of any separate license agreement you may have executed\n   with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n   names, trademarks, service marks, or product names of the Licensor,\n   except as required for reasonable and customary use in describing the\n   origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n   agreed to in writing, Licensor provides the Work (and each\n   Contributor provides its Contributions) on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n   implied, including, without limitation, any warranties or conditions\n   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n   PARTICULAR PURPOSE. You are solely responsible for determining the\n   appropriateness of using or redistributing the Work and assume any\n   risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n   whether in tort (including negligence), contract, or otherwise,\n   unless required by applicable law (such as deliberate and grossly\n   negligent acts) or agreed to in writing, shall any Contributor be\n   liable to You for damages, including any direct, indirect, special,\n   incidental, or consequential damages of any character arising as a\n   result of this License or out of the use or inability to use the\n   Work (including but not limited to damages for loss of goodwill,\n   work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses), even if such Contributor\n   has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n   the Work or Derivative Works thereof, You may choose to offer,\n   and charge a fee for, acceptance of support, warranty, indemnity,\n   or other liability obligations and/or rights consistent with this\n   License. However, in accepting such obligations, You may act only\n   on Your own behalf and on Your sole responsibility, not on behalf\n   of any other Contributor, and only if You agree to indemnify,\n   defend, and hold each Contributor harmless for any liability\n   incurred by, or claims asserted against, such Contributor by reason\n   of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\n--------------------\nLicense notice for unicode-ident\n--------------------\nUNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE\n\nSee Terms of Use <https://www.unicode.org/copyright.html>\nfor definitions of Unicode Inc.’s Data Files and Software.\n\nNOTICE TO USER: Carefully read the following legal agreement.\nBY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S\nDATA FILES (\"DATA FILES\"), AND/OR SOFTWARE (\"SOFTWARE\"),\nYOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE\nTERMS AND CONDITIONS OF THIS AGREEMENT.\nIF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE\nTHE DATA FILES OR SOFTWARE.\n\nCOPYRIGHT AND PERMISSION NOTICE\n\nCopyright © 1991-2022 Unicode, Inc. All rights reserved.\nDistributed under the Terms of Use in https://www.unicode.org/copyright.html.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Unicode data files and any associated documentation\n(the \"Data Files\") or Unicode software and any associated documentation\n(the \"Software\") to deal in the Data Files or Software\nwithout restriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, and/or sell copies of\nthe Data Files or Software, and to permit persons to whom the Data Files\nor Software are furnished to do so, provided that either\n(a) this copyright and permission notice appear with all copies\nof the Data Files or Software, or\n(b) this copyright and permission notice appear in associated\nDocumentation.\n\nTHE DATA FILES AND SOFTWARE ARE PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\nWARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT OF THIRD PARTY RIGHTS.\nIN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS\nNOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL\nDAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,\nDATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\nTORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THE DATA FILES OR SOFTWARE.\n\nExcept as contained in this notice, the name of a copyright holder\nshall not be used in advertising or otherwise to promote the sale,\nuse or other dealings in these Data Files or Software without prior\nwritten authorization of the copyright holder.\n\n--------------------\nLicense notice for Selenium Atoms\n--------------------\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2022 Software Freedom Conservancy (SFC)\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------\nLicense notice for Sizzle\n--------------------\nMIT License\n----\n\nCopyright (c) 2009 John Resig\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n--------------------\nLicense notice for Wicked Good XPath\n--------------------\nThe MIT License\n\nCopyright (c) 2007 Cybozu Labs, Inc.\nCopyright (c) 2012 Google Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n--------------------\nLicense notice for sqlite\n--------------------\nThe author disclaims copyright to this source code.  In place of\na legal notice, here is a blessing:\n\n   May you do good and not evil.\n   May you find forgiveness for yourself and forgive others.\n   May you share freely, never taking more than you give.\n\n--------------------\nLicense notice for Vulkan API headers\n--------------------\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------\nLicense notice for WebRTC\n--------------------\nCopyright (c) 2011, The WebRTC project authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n  * Redistributions of source code must retain the above copyright\n    notice, this list of conditions and the following disclaimer.\n\n  * Redistributions in binary form must reproduce the above copyright\n    notice, this list of conditions and the following disclaimer in\n    the documentation and/or other materials provided with the\n    distribution.\n\n  * Neither the name of Google nor the names of its contributors may\n    be used to endorse or promote products derived from this software\n    without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n--------------------\nLicense notice for General Purpose FFT (Fast Fourier/Cosine/Sine Transform) Package\n--------------------\n/*\n * http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html\n * Copyright Takuya OOURA, 1996-2001\n *\n * You may use, copy, modify and distribute this code for any purpose (include\n * commercial use) and without fee. Please refer to this package when you modify\n * this code.\n */\n\n--------------------\nLicense notice for sql sqrt floor\n--------------------\n/*\n * Written by Wilco Dijkstra, 1996. The following email exchange establishes the\n * license.\n *\n * From: Wilco Dijkstra <Wilco.Dijkstra@ntlworld.com>\n * Date: Fri, Jun 24, 2011 at 3:20 AM\n * Subject: Re: sqrt routine\n * To: Kevin Ma <kma@google.com>\n * Hi Kevin,\n * Thanks for asking. Those routines are public domain (originally posted to\n * comp.sys.arm a long time ago), so you can use them freely for any purpose.\n * Cheers,\n * Wilco\n *\n * ----- Original Message -----\n * From: \"Kevin Ma\" <kma@google.com>\n * To: <Wilco.Dijkstra@ntlworld.com>\n * Sent: Thursday, June 23, 2011 11:44 PM\n * Subject: Fwd: sqrt routine\n * Hi Wilco,\n * I saw your sqrt routine from several web sites, including\n * http://www.finesse.demon.co.uk/steven/sqrt.html.\n * Just wonder if there's any copyright information with your Successive\n * approximation routines, or if I can freely use it for any purpose.\n * Thanks.\n * Kevin\n */\n\n--------------------\nLicense notice for C++ Signal/Slot Library\n--------------------\n// sigslot.h: Signal/Slot classes\n//\n// Written by Sarah Thompson (sarah@telergy.com) 2002.\n//\n// License: Public domain. You are free to use this code however you like, with\n// the proviso that the author takes on no responsibility or liability for any\n// use.\n\n--------------------\nLicense notice for Wuffs (Wrangling Untrusted File Formats Safely)\n--------------------\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------\nLicense notice for zlib\n--------------------\nversion 1.2.12, March 27th, 2022\n\nCopyright (C) 1995-2022 Jean-loup Gailly and Mark Adler\n\nThis software is provided 'as-is', without any express or implied\nwarranty.  In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.\n\n--------------------\nLicense notice for Zstandard\n--------------------\nBSD License\n\nFor Zstandard software\n\nCopyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n * Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n * Neither the name Facebook, nor Meta, nor the names of its contributors may\n   be used to endorse or promote products derived from this software without\n   specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "api/src/config.ts",
    "content": "import { FastifyServerOptions } from \"fastify\";\nimport { env } from \"./env.js\";\nimport stringify from \"json-stringify-safe\";\n\ninterface LoggingConfig {\n  [key: string]: FastifyServerOptions[\"logger\"];\n}\n\nexport const loggingConfig: LoggingConfig = {\n  development: {\n    transport: {\n      target: \"pino-pretty\",\n      options: {\n        translateTime: \"HH:MM:ss Z\",\n        ignore: \"pid,hostname\",\n      },\n    },\n    ...(env.ENABLE_VERBOSE_LOGGING\n      ? {\n          hooks: {\n            logMethod(inputArgs: any[], method: any) {\n              if (inputArgs.length > 1) {\n                try {\n                  let resultingMessage = \"\";\n                  if (typeof inputArgs[0] === \"string\") {\n                    const [message, ...args] = inputArgs;\n                    resultingMessage = `${message} ${stringify(args)}`;\n                  } else {\n                    resultingMessage = stringify(inputArgs);\n                  }\n                  return method.apply(this, [resultingMessage]);\n                } catch (error) {\n                  console.error(\n                    \"Error trying to process logs with verbose logging enabled: \",\n                    error,\n                  );\n                }\n              }\n              return method.apply(this, inputArgs);\n            },\n          },\n        }\n      : {}),\n    level: process.env.LOG_LEVEL || \"debug\",\n  },\n  production: {},\n  test: false,\n};\n"
  },
  {
    "path": "api/src/env.ts",
    "content": "import { z } from \"zod\";\nimport { config } from \"dotenv\";\n\nconfig();\n\nconst envSchema = z.object({\n  NODE_ENV: z\n    .enum([\"test\", \"development\", \"staging\", \"production\", \"preview\"])\n    .default(\"development\"),\n  HOST: z.string().optional().default(\"0.0.0.0\"),\n  DOMAIN: z.string().optional(),\n  PORT: z.string().optional().default(\"3000\"),\n  USE_SSL: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"false\"),\n  CDP_REDIRECT_PORT: z.string().optional().default(\"9222\"),\n  CDP_DOMAIN: z.string().optional(),\n  PROXY_URL: z.string().optional(),\n  DEFAULT_HEADERS: z\n    .string()\n    .optional()\n    .transform((val) => (val ? JSON.parse(val) : {}))\n    .pipe(z.record(z.string()).optional().default({})),\n  KILL_TIMEOUT: z.string().optional().default(\"0\"),\n  CHROME_EXECUTABLE_PATH: z.string().optional(),\n  CHROME_HEADLESS: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"true\"),\n  DISPLAY: z.string().optional().default(\":10\"),\n  ENABLE_CDP_LOGGING: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"false\"),\n  LOG_CUSTOM_EMIT_EVENTS: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"false\"),\n  ENABLE_VERBOSE_LOGGING: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"false\"),\n  DEFAULT_TIMEZONE: z.string().optional(),\n  TIMEZONE_SERVICE_URL: z.string().optional(),\n  SKIP_FINGERPRINT_INJECTION: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"false\"),\n  CHROME_ARGS: z\n    .string()\n    .optional()\n    .transform((val) => (val ? val.split(\" \").map((arg) => arg.trim()) : []))\n    .default(\"\"),\n  FILTER_CHROME_ARGS: z\n    .string()\n    .optional()\n    .transform((val) => (val ? val.split(\" \").map((arg) => arg.trim()) : []))\n    .default(\"\"),\n  DEBUG_CHROME_PROCESS: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"false\"),\n  PROXY_INTERNAL_BYPASS: z.string().optional(),\n  CHROME_USER_DATA_DIR: z.string().optional(),\n  LOG_STORAGE_ENABLED: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"false\"),\n  LOG_STORAGE_PATH: z.string().optional(),\n  DISABLE_CHROME_SANDBOX: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\" || val === \"1\")\n    .default(\"false\"),\n});\n\nexport const env = envSchema.parse(process.env);\n"
  },
  {
    "path": "api/src/index.ts",
    "content": "import fastify from \"fastify\";\nimport fastifyCors from \"@fastify/cors\";\nimport fastifySensible from \"@fastify/sensible\";\nimport steelBrowserPlugin from \"./steel-browser-plugin.js\";\nimport uiPlugin from \"./plugins/ui-plugin.js\";\nimport { loggingConfig } from \"./config.js\";\nimport { MB } from \"./utils/size.js\";\nimport path from \"node:path\";\n\nconst HOST = process.env.HOST ?? \"0.0.0.0\";\nconst PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;\n\nexport const server = fastify({\n  logger: loggingConfig[process.env.NODE_ENV ?? \"development\"] ?? true,\n  trustProxy: true,\n  bodyLimit: 100 * MB,\n  disableRequestLogging: true,\n});\n\nconst setupServer = async () => {\n  await server.register(fastifySensible);\n  await server.register(fastifyCors, { origin: true });\n\n  // Register UI plugin only in production (when we have built UI files)\n  if (process.env.NODE_ENV === \"production\") {\n    await server.register(uiPlugin, {\n      uiDistPath: path.join(process.cwd(), \"ui/dist\"),\n      uiPrefix: \"/ui\",\n    });\n  }\n\n  await server.register(steelBrowserPlugin, {\n    fileStorage: {\n      maxSizePerSession: 100 * MB,\n    },\n  });\n};\n\nconst startServer = async () => {\n  try {\n    await setupServer();\n    await server.listen({ port: PORT, host: HOST });\n  } catch (err) {\n    server.log.error(err);\n    process.exit(1);\n  }\n};\n\nstartServer();\n"
  },
  {
    "path": "api/src/modules/actions/actions.controller.ts",
    "content": "import { FastifyReply } from \"fastify\";\nimport { BrowserContext, Page, HTTPResponse } from \"puppeteer-core\";\nimport { CDPService } from \"../../services/cdp/cdp.service.js\";\nimport { SessionService } from \"../../services/session.service.js\";\nimport { ScrapeFormat } from \"../../types/index.js\";\nimport { getErrors } from \"../../utils/errors.js\";\nimport { updateLog } from \"../../utils/logging.js\";\nimport { IProxyServer } from \"../../utils/proxy.js\";\nimport {\n  cleanHtml,\n  getDefuddleContent,\n  htmlToMarkdown,\n  transformHtml,\n} from \"../../utils/scrape/index.js\";\nimport { normalizeUrl } from \"../../utils/url.js\";\nimport { PDFRequest, ScrapeRequest, ScreenshotRequest, SearchRequest } from \"./actions.schema.js\";\nimport { DefuddleResponse } from \"defuddle\";\nimport pdf2html from \"pdf2html\";\nimport {\n  buildHtmlLikeMetadataFromPdf,\n  extractLinksFromConvertedHtml,\n} from \"../../utils/scrape/pdfToHtml.js\";\nimport { safeGoto } from \"../../utils/scrape/safeGoTo.js\";\n\nexport const handleScrape = async (\n  sessionService: SessionService,\n  browserService: CDPService,\n  request: ScrapeRequest,\n  reply: FastifyReply,\n) => {\n  const startTime = Date.now();\n  let times: Record<string, number> = {};\n  const { url, format, screenshot, pdf, proxyUrl, logUrl, delay } = request.body;\n\n  let proxy: IProxyServer | null = null;\n  let context: BrowserContext | null = null;\n\n  try {\n    if (proxyUrl) {\n      proxy = await sessionService.proxyFactory(proxyUrl);\n      await proxy.listen();\n    }\n    times.proxyTime = Date.now() - startTime;\n\n    let page: Page;\n    let response: HTTPResponse | null = null;\n    let pdfResponse: HTTPResponse | null = null;\n    let isPdfNavigation = false;\n\n    if (!browserService.isRunning()) {\n      await browserService.launch();\n    }\n\n    if (proxy) {\n      // If a proxy is used, we proceed with browser navigation; implementing proxy-aware Node fetch\n      // would require an HTTP agent and is outside current scope.\n      context = await browserService.createBrowserContext(proxy.url);\n      page = await context.newPage();\n      times.proxyPageTime = Date.now() - startTime - times.proxyTime;\n    } else {\n      page = await browserService.getPrimaryPage();\n      times.pageTime = Date.now() - startTime - times.proxyTime;\n    }\n\n    // PDF retrieval will use node fetch with session cookies; removed CDP tracking\n\n    let normalizedUrl: string | null = null;\n    if (url) {\n      normalizedUrl = normalizeUrl(url);\n      if (!normalizedUrl) {\n        throw new Error(`Invalid URL: ${url}`);\n      }\n    }\n\n    const safeResponse = normalizedUrl\n      ? await safeGoto(page, normalizedUrl, {\n          timeout: 30000,\n          waitUntil: \"domcontentloaded\",\n        })\n      : { response: null, isPdf: false, pdfResponse: null };\n\n    response = safeResponse.response !== null ? safeResponse.response : safeResponse.pdfResponse;\n    pdfResponse = safeResponse.pdfResponse;\n    const isPdf = safeResponse.isPdf;\n\n    if (delay) {\n      await new Promise((resolve) => setTimeout(resolve, delay));\n    }\n\n    const contentType = response?.headers()[\"content-type\"]?.toLowerCase() || \"\";\n\n    let scrapeResponse: Record<string, any> = {};\n    let htmlContent = \"\";\n    let cleanedHtml: string;\n    let readabilityContent: DefuddleResponse;\n\n    if (isPdf || contentType.includes(\"application/pdf\")) {\n      // Node fetch using session cookies (same browser auth state)\n      const targetUrl = normalizedUrl || url!;\n      const cookies = await page.cookies(targetUrl);\n      const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join(\"; \");\n      const fetchHeaders: Record<string, string> = {};\n      if (cookieHeader) fetchHeaders[\"Cookie\"] = cookieHeader;\n      if (!fetchHeaders[\"Referer\"]) {\n        const u = new URL(targetUrl);\n        fetchHeaders[\"Referer\"] = u.origin + \"/\";\n      }\n      const nodeRes = await fetch(targetUrl, {\n        method: \"GET\",\n        redirect: \"follow\",\n        headers: fetchHeaders,\n      });\n      const nodeCT = (nodeRes.headers.get(\"content-type\") || \"\").toLowerCase();\n      if (!nodeRes.ok || !nodeCT.includes(\"application/pdf\")) {\n        throw new Error(`Expected PDF; got status ${nodeRes.status} content-type ${nodeCT}`);\n      }\n      const arrBuf = await nodeRes.arrayBuffer();\n      const pdfBuffer = Buffer.from(arrBuf);\n\n      const convertStart = Date.now();\n      htmlContent = await pdf2html.html(pdfBuffer);\n      times.pdfHtmlConvertTime = Date.now() - convertStart;\n\n      const metaStart = Date.now();\n      const pdfMeta = await pdf2html.meta(pdfBuffer);\n      times.pdfMetaTime = Date.now() - metaStart;\n\n      const htmlMeta = buildHtmlLikeMetadataFromPdf(pdfMeta, {\n        urlSource: targetUrl,\n        statusCode: nodeRes.status,\n        htmlForFallback: htmlContent,\n      });\n\n      const htmlLinks = extractLinksFromConvertedHtml(htmlContent);\n\n      scrapeResponse = {\n        content: {},\n        metadata: {\n          ...htmlMeta,\n          statusCode: nodeRes.status,\n          headers: Object.fromEntries(nodeRes.headers.entries()),\n          originalContentType: nodeCT,\n          pdfAcquisition: \"node-fetch-with-cookies\",\n        },\n        links: htmlLinks,\n      };\n\n      if (pdf) {\n        scrapeResponse.pdf = pdfBuffer.toString(\"base64\");\n      }\n    } else {\n      // Regular HTML flow\n      await page.evaluate(() => {\n        (window as any).__name = (func: Function) => func;\n      });\n\n      const [{ html, metadata, links }, base64Screenshot, pdfBuffer] = await Promise.all([\n        page.evaluate(() => {\n          const getMetaContent = (selector: string) => {\n            const element = document.querySelector(selector);\n            return element ? element.getAttribute(\"content\") : null;\n          };\n          const getMetaByName = (name: string) => getMetaContent(`meta[name=\"${name}\"]`);\n          const getMetaByProperty = (property: string) =>\n            getMetaContent(`meta[property=\"${property}\"]`);\n\n          const extractJsonLd = () => {\n            const scripts = document.querySelectorAll('script[type=\"application/ld+json\"]');\n            const jsonLdData: any[] = [];\n            scripts.forEach((script) => {\n              try {\n                const data = JSON.parse(script.textContent || \"\");\n                jsonLdData.push(data);\n              } catch (e) {\n                console.error(e);\n              }\n            });\n            return jsonLdData;\n          };\n\n          return {\n            html: document.documentElement.outerHTML,\n            links: [...document.links].map((l) => ({\n              url: l.href,\n              text: l.textContent?.trim() || \"\",\n            })),\n            metadata: {\n              title: document.title,\n              language: document.documentElement.lang,\n              urlSource: window.location.href,\n              timestamp: new Date().toISOString(),\n\n              description: getMetaByName(\"description\"),\n              keywords: getMetaByName(\"keywords\"),\n              author: getMetaByName(\"author\"),\n\n              ogTitle: getMetaByProperty(\"og:title\"),\n              ogDescription: getMetaByProperty(\"og:description\"),\n              ogImage: getMetaByProperty(\"og:image\"),\n              ogUrl: getMetaByProperty(\"og:url\"),\n              ogSiteName: getMetaByProperty(\"og:site_name\"),\n\n              articleAuthor: getMetaByProperty(\"article:author\"),\n              publishedTime: getMetaByProperty(\"article:published_time\"),\n              modifiedTime: getMetaByProperty(\"article:modified_time\"),\n\n              canonical: document.querySelector('link[rel=\"canonical\"]')?.getAttribute(\"href\"),\n              favicon: document.querySelector('link[rel=\"icon\"]')?.getAttribute(\"href\"),\n\n              jsonLd: extractJsonLd(),\n              statusCode: 200,\n            },\n          };\n        }),\n        screenshot ? page.screenshot({ encoding: \"base64\", type: \"jpeg\", quality: 100 }) : null,\n        pdf ? page.pdf() : null,\n      ]);\n\n      htmlContent = html;\n      times.extractionTime = Date.now() - startTime - (times.pageLoadTime || 0);\n\n      scrapeResponse = { content: {}, metadata, links };\n\n      if (base64Screenshot) {\n        scrapeResponse.screenshot = base64Screenshot;\n      }\n      if (pdfBuffer) {\n        scrapeResponse.pdf = Buffer.from(pdfBuffer).toString(\"base64\");\n      }\n    }\n\n    // Format handling (works for both PDF converted HTML and normal HTML)\n    if (format && format.length > 0) {\n      if (format.includes(ScrapeFormat.HTML)) {\n        scrapeResponse.content.html = htmlContent;\n      }\n\n      const needsCleanedHtml = format.includes(ScrapeFormat.CLEANED_HTML);\n      const needsReadability =\n        format.includes(ScrapeFormat.READABILITY) || format.includes(ScrapeFormat.MARKDOWN);\n\n      if (needsCleanedHtml) {\n        const cleanHtmlStart = Date.now();\n        cleanedHtml = cleanHtml(htmlContent);\n        times.cleanedHtmlTime = Date.now() - cleanHtmlStart;\n\n        if (format.includes(ScrapeFormat.CLEANED_HTML)) {\n          scrapeResponse.content.cleaned_html = cleanedHtml;\n        }\n      }\n\n      if (needsReadability) {\n        const readabilityStart = Date.now();\n        readabilityContent = await getDefuddleContent(\n          transformHtml(htmlContent, normalizedUrl || url),\n        );\n        times.readabilityTime = Date.now() - readabilityStart;\n\n        if (format.includes(ScrapeFormat.READABILITY)) {\n          scrapeResponse.content.readability = readabilityContent.content;\n        }\n      }\n\n      if (format.includes(ScrapeFormat.MARKDOWN)) {\n        const markdownStart = Date.now();\n        scrapeResponse.content.markdown = await htmlToMarkdown(readabilityContent!.content);\n        times.markdownTime = Date.now() - markdownStart;\n      }\n    } else {\n      scrapeResponse.content.html = htmlContent;\n    }\n\n    times.totalInstanceTime = Date.now() - startTime;\n\n    if (logUrl) {\n      await updateLog(logUrl, { times });\n    }\n\n    return reply.send(scrapeResponse);\n  } catch (e: unknown) {\n    const error = getErrors(e);\n\n    if (logUrl) {\n      await updateLog(logUrl, { times, response: { browserError: error } });\n    }\n\n    if (url) {\n      await browserService.refreshPrimaryPage();\n    }\n    return reply.code(500).send({ message: error });\n  } finally {\n    if (context) {\n      await context.close().catch(() => {});\n    }\n    if (proxy) {\n      await proxy.close(true).catch(() => {});\n    }\n  }\n};\n\nexport const handleSearch = async (\n  sessionService: SessionService,\n  browserService: CDPService,\n  request: SearchRequest,\n  reply: FastifyReply,\n) => {\n  const startTime = Date.now();\n  let times: Record<string, number> = {};\n  const { query, proxyUrl, logUrl } = request.body;\n\n  let proxy: IProxyServer | null = null;\n  let context: BrowserContext | null = null;\n\n  try {\n    if (proxyUrl) {\n      proxy = await sessionService.proxyFactory(proxyUrl);\n      await proxy.listen();\n    }\n    times.proxyTime = Date.now() - startTime;\n\n    let page: Page;\n\n    if (!browserService.isRunning()) {\n      await browserService.launch();\n    }\n\n    if (proxy) {\n      // If a proxy is used, we proceed with browser navigation; implementing proxy-aware Node fetch\n      // would require an HTTP agent and is outside current scope.\n      context = await browserService.createBrowserContext(proxy.url);\n      page = await context.newPage();\n      times.proxyPageTime = Date.now() - startTime - times.proxyTime;\n    } else {\n      page = await browserService.getPrimaryPage();\n      times.pageTime = Date.now() - startTime - times.proxyTime;\n    }\n\n    await page.evaluate(() => {\n      (window as any).__name = (func: Function) => func;\n    });\n\n    // Go to Brave\n    await page.goto(`https://search.brave.com/search?q=${encodeURIComponent(query)}`, {\n      waitUntil: \"networkidle2\",\n    });\n\n    // Wait for results to load\n    await page.waitForSelector(\"#results\");\n\n    // Scrape results\n    const results = await page.evaluate(() => {\n      const items = document.querySelectorAll(\"div.snippet\");\n\n      return Array.from(items)\n        .map((item) => {\n          if (\n            [\n              \"llm-snippet\",\n              \"faq\",\n              \"pagination-snippet\",\n              \"search-elsewhere\",\n              \"infoblox-snippet\",\n              \"discussions\",\n            ].includes(item.id)\n          ) {\n            return;\n          }\n          const urlEl = item.querySelector(\"div.result-content a\");\n          const descEl = item.querySelector(\"div.generic-snippet\");\n          const titleEl = item.querySelector(\"div.result-content a div.title\");\n\n          return {\n            title: titleEl?.textContent?.trim() || null,\n            url: urlEl?.getAttribute(\"href\") || null,\n            description: descEl?.textContent?.split(\"-\")[1]?.trim() || null,\n          };\n        })\n        .filter(\n          (item) =>\n            item &&\n            typeof item === \"object\" &&\n            \"title\" in item &&\n            \"url\" in item &&\n            \"description\" in item &&\n            item.title !== null &&\n            item.url !== null,\n        );\n    });\n    times.totalInstanceTime = Date.now() - startTime;\n\n    if (logUrl) {\n      await updateLog(logUrl, { times });\n    }\n\n    return reply.send({ results });\n  } catch (e: unknown) {\n    const error = getErrors(e);\n\n    if (logUrl) {\n      await updateLog(logUrl, { times, response: { browserError: error } });\n    }\n\n    return reply.code(500).send({ message: error });\n  } finally {\n    if (context) {\n      await context.close().catch(() => {});\n    }\n    if (proxy) {\n      await proxy.close(true).catch(() => {});\n    }\n  }\n};\n\nexport const handleScreenshot = async (\n  sessionService: SessionService,\n  browserService: CDPService,\n  request: ScreenshotRequest,\n  reply: FastifyReply,\n) => {\n  const startTime = Date.now();\n  let times: Record<string, number> = {};\n  const { url, logUrl, proxyUrl, delay, fullPage } = request.body;\n\n  let proxy: IProxyServer | null = null;\n  let context: BrowserContext | null = null;\n\n  if (!browserService.isRunning()) {\n    await browserService.launch();\n  }\n\n  try {\n    if (proxyUrl) {\n      proxy = await sessionService.proxyFactory(proxyUrl);\n      await proxy.listen();\n    }\n\n    times.proxyTime = Date.now() - startTime;\n\n    let page: Page;\n\n    if (proxy) {\n      context = await browserService.createBrowserContext(proxy.url);\n      page = await context.newPage();\n      times.proxyPageTime = Date.now() - startTime - times.proxyTime;\n    } else {\n      page = await browserService.getPrimaryPage();\n      times.pageTime = Date.now() - startTime;\n    }\n\n    if (url) {\n      const normalizedUrl = normalizeUrl(url);\n      if (!normalizedUrl) {\n        throw new Error(`Invalid URL: ${url}`);\n      }\n      await page.goto(normalizedUrl, { timeout: 30000, waitUntil: \"domcontentloaded\" });\n      times.pageLoadTime = Date.now() - times.pageTime - times.proxyTime - startTime;\n    }\n\n    if (delay) {\n      await new Promise((resolve) => setTimeout(resolve, delay));\n    }\n\n    const screenshot = await page.screenshot({ fullPage, type: \"jpeg\", quality: 100 });\n    times.screenshotTime =\n      Date.now() - times.pageLoadTime - times.pageTime - times.proxyTime - startTime;\n\n    if (logUrl) {\n      await updateLog(logUrl, { times });\n    }\n\n    return reply.send(screenshot);\n  } catch (e: unknown) {\n    const error = getErrors(e);\n\n    if (logUrl) {\n      await updateLog(logUrl, { times, response: { browserError: error } });\n    }\n\n    if (url) {\n      await browserService.refreshPrimaryPage();\n    }\n\n    return reply.code(500).send({ message: error });\n  } finally {\n    if (context) {\n      await context.close().catch(() => {});\n    }\n    if (proxy) {\n      await proxy.close(true).catch(() => {});\n    }\n  }\n};\n\nexport const handlePDF = async (\n  sessionService: SessionService,\n  browserService: CDPService,\n  request: PDFRequest,\n  reply: FastifyReply,\n) => {\n  const startTime = Date.now();\n  let times: Record<string, number> = {};\n  const { url, logUrl, proxyUrl, delay } = request.body;\n\n  let proxy: IProxyServer | null = null;\n  let context: BrowserContext | null = null;\n\n  if (!browserService.isRunning()) {\n    await browserService.launch();\n  }\n\n  try {\n    if (proxyUrl) {\n      proxy = await sessionService.proxyFactory(proxyUrl);\n      await proxy.listen();\n    }\n\n    times.proxyTime = Date.now() - startTime;\n\n    let page: Page;\n\n    if (proxy) {\n      context = await browserService.createBrowserContext(proxy.url);\n      page = await context.newPage();\n      times.proxyPageTime = Date.now() - startTime - times.proxyTime;\n    } else {\n      page = await browserService.getPrimaryPage();\n      times.pageTime = Date.now() - startTime;\n    }\n\n    if (url) {\n      const normalizedUrl = normalizeUrl(url);\n      if (!normalizedUrl) {\n        throw new Error(`Invalid URL: ${url}`);\n      }\n      await page.goto(normalizedUrl, { timeout: 30000, waitUntil: \"domcontentloaded\" });\n      times.pageLoadTime = Date.now() - times.pageTime - times.proxyTime - startTime;\n    }\n\n    if (delay) {\n      await new Promise((resolve) => setTimeout(resolve, delay));\n    }\n\n    const pdf = await page.pdf();\n    times.pdfTime = Date.now() - times.pageLoadTime - times.pageTime - times.proxyTime - startTime;\n\n    if (logUrl) {\n      await updateLog(logUrl, { times });\n    }\n\n    return reply.send(pdf);\n  } catch (e: unknown) {\n    const error = getErrors(e);\n\n    if (logUrl) {\n      await updateLog(logUrl, { times, response: { browserError: error } });\n    }\n\n    if (url) {\n      await browserService.refreshPrimaryPage();\n    }\n\n    return reply.code(500).send({ message: error });\n  } finally {\n    if (context) {\n      await context.close().catch(() => {});\n    }\n    if (proxy) {\n      await proxy.close(true).catch(() => {});\n    }\n  }\n};\n"
  },
  {
    "path": "api/src/modules/actions/actions.routes.ts",
    "content": "import { FastifyInstance, FastifyReply } from \"fastify\";\nimport { handlePDF, handleScrape, handleScreenshot, handleSearch } from \"./actions.controller.js\";\nimport { $ref } from \"../../plugins/schemas.js\";\nimport { PDFRequest, ScrapeRequest, ScreenshotRequest, SearchRequest } from \"./actions.schema.js\";\n\nasync function routes(server: FastifyInstance) {\n  server.post(\n    \"/scrape\",\n    {\n      schema: {\n        operationId: \"scrape\",\n        description: \"Scrape a URL\",\n        tags: [\"Browser Actions\"],\n        summary: \"Scrape a URL\",\n        body: $ref(\"ScrapeRequest\"),\n        response: {\n          200: $ref(\"ScrapeResponse\"),\n        },\n      },\n    },\n    async (request: ScrapeRequest, reply: FastifyReply) =>\n      handleScrape(server.sessionService, server.cdpService, request, reply),\n  );\n\n  server.post(\n    \"/screenshot\",\n    {\n      schema: {\n        operationId: \"screenshot\",\n        description: \"Take a screenshot\",\n        tags: [\"Browser Actions\"],\n        summary: \"Take a screenshot\",\n        body: $ref(\"ScreenshotRequest\"),\n        response: {\n          200: $ref(\"ScreenshotResponse\"),\n        },\n      },\n    },\n    async (request: ScreenshotRequest, reply: FastifyReply) =>\n      handleScreenshot(server.sessionService, server.cdpService, request, reply),\n  );\n\n  server.post(\n    \"/pdf\",\n    {\n      schema: {\n        operationId: \"pdf\",\n        description: \"Get the PDF content of a page\",\n        tags: [\"Browser Actions\"],\n        summary: \"Get the PDF content of a page\",\n        body: $ref(\"PDFRequest\"),\n        response: {\n          200: $ref(\"PDFResponse\"),\n        },\n      },\n    },\n    async (request: PDFRequest, reply: FastifyReply) =>\n      handlePDF(server.sessionService, server.cdpService, request, reply),\n  );\n\n  server.post(\n    \"/search\",\n    {\n      schema: {\n        operationId: \"search\",\n        description: \"Use a search engine to search for URLs given a query\",\n        tags: [\"Browser Actions\"],\n        summary: \"Use a search engine to search for URLs given a query\",\n        body: $ref(\"SearchRequest\"),\n        response: {\n          200: $ref(\"SearchResponse\"),\n        },\n      },\n    },\n    async (request: SearchRequest, reply: FastifyReply) =>\n      handleSearch(server.sessionService, server.cdpService, request, reply),\n  );\n}\n\nexport default routes;\n"
  },
  {
    "path": "api/src/modules/actions/actions.schema.ts",
    "content": "import { FastifyRequest } from \"fastify\";\nimport { z } from \"zod\";\nimport { ScrapeFormat } from \"../../types/enums.js\";\n\nconst ScrapeRequest = z.object({\n  url: z.string().optional(),\n  format: z.array(z.nativeEnum(ScrapeFormat)).optional(),\n  screenshot: z.boolean().optional(),\n  pdf: z.boolean().optional(),\n  proxyUrl: z\n    .string()\n    .nullable()\n    .optional()\n    .describe(\n      \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\",\n    ),\n  delay: z.number().optional(),\n  logUrl: z.string().optional(),\n});\n\nconst ScrapeResponse = z.object({\n  content: z.record(z.nativeEnum(ScrapeFormat), z.any()),\n  metadata: z.object({\n    title: z.string().optional(),\n    language: z.string().optional(),\n    urlSource: z.string().optional(),\n    timestamp: z.string().datetime().optional(),\n\n    description: z.string().optional(),\n    keywords: z.string().optional(),\n    author: z.string().optional(),\n\n    ogTitle: z.string().optional(),\n    ogDescription: z.string().optional(),\n    ogImage: z.string().optional(),\n    ogUrl: z.string().optional(),\n    ogSiteName: z.string().optional(),\n\n    articleAuthor: z.string().optional(),\n    publishedTime: z.string().optional(),\n    modifiedTime: z.string().optional(),\n\n    canonical: z.string().optional(),\n    favicon: z.string().optional(),\n\n    jsonLd: z.any().optional(),\n    statusCode: z.number().int(),\n  }),\n  links: z.array(\n    z.object({\n      url: z.string(),\n      text: z.string(),\n    }),\n  ),\n  screenshot: z.string().optional(),\n  pdf: z.string().optional(),\n});\n\nconst ScreenshotRequest = z.object({\n  url: z.string().optional(),\n  proxyUrl: z\n    .string()\n    .nullable()\n    .optional()\n    .describe(\n      \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\",\n    ),\n  delay: z.number().optional(),\n  fullPage: z.boolean().optional(),\n  logUrl: z.string().optional(),\n});\n\nconst ScreenshotResponse = z.any();\n\nconst PDFRequest = z.object({\n  url: z.string().optional(),\n  proxyUrl: z\n    .string()\n    .nullable()\n    .optional()\n    .describe(\n      \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\",\n    ),\n  delay: z.number().optional(),\n  logUrl: z.string().optional(),\n});\n\nconst SearchRequest = z.object({\n  query: z.string(),\n  proxyUrl: z\n    .string()\n    .nullable()\n    .optional()\n    .describe(\n      \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\",\n    ),\n  logUrl: z.string().optional(),\n});\n\nconst SearchResponse = z.object({\n  results: z.array(\n    z.object({\n      title: z.string(),\n      url: z.string(),\n      description: z.string().nullable().optional(),\n    }),\n  ),\n});\n\nconst PDFResponse = z.any();\n\nexport type ScrapeRequestBody = z.infer<typeof ScrapeRequest>;\nexport type ScrapeRequest = FastifyRequest<{ Body: ScrapeRequestBody }>;\n\nexport type ScreenshotRequestBody = z.infer<typeof ScreenshotRequest>;\nexport type ScreenshotRequest = FastifyRequest<{ Body: ScreenshotRequestBody }>;\n\nexport type PDFRequestBody = z.infer<typeof PDFRequest>;\nexport type PDFRequest = FastifyRequest<{ Body: PDFRequestBody }>;\n\nexport type SearchRequestBody = z.infer<typeof SearchRequest>;\nexport type SearchRequest = FastifyRequest<{ Body: SearchRequestBody }>;\n\nexport const actionsSchemas = {\n  ScrapeRequest,\n  ScrapeResponse,\n  ScreenshotRequest,\n  ScreenshotResponse,\n  PDFRequest,\n  PDFResponse,\n  SearchRequest,\n  SearchResponse,\n};\n\nexport default actionsSchemas;\n"
  },
  {
    "path": "api/src/modules/cdp/cdp.routes.ts",
    "content": "import { FastifyInstance, FastifyRequest, FastifyReply } from \"fastify\";\nimport { z } from \"zod\";\nimport { $ref } from \"../../plugins/schemas.js\";\nimport cdpSchemas from \"./cdp.schemas.js\";\n\nasync function routes(server: FastifyInstance) {\n  server.get(\n    \"/devtools/inspector.html\",\n    {\n      schema: {\n        operationId: \"getDevtoolsUrl\",\n        description: \"Get the URL for the DevTools inspector\",\n        tags: [\"CDP\"],\n        summary: \"Get the URL for the DevTools inspector\",\n        querystring: $ref(\"GetDevtoolsUrlSchema\"),\n      },\n    },\n    async (\n      request: FastifyRequest<{ Querystring: z.infer<typeof cdpSchemas.GetDevtoolsUrlSchema> }>,\n      reply: FastifyReply,\n    ) => {\n      return reply.redirect(\n        `${server.cdpService.getDebuggerUrl()}?ws=${server.cdpService\n          .getDebuggerWsUrl(request.query.pageId)\n          .replace(\"ws:\", \"\")}`,\n      );\n    },\n  );\n}\n\nexport default routes;\n"
  },
  {
    "path": "api/src/modules/cdp/cdp.schemas.ts",
    "content": "import { z } from \"zod\";\n\nexport const GetDevtoolsUrlSchema = z.object({\n  pageId: z.string().optional(),\n});\n\nexport default {\n  GetDevtoolsUrlSchema,\n};\n"
  },
  {
    "path": "api/src/modules/files/files.controller.ts",
    "content": "import { MultipartFile } from \"@fastify/multipart\";\nimport archiver from \"archiver\";\nimport { randomUUID } from \"crypto\";\nimport { FastifyInstance, FastifyReply, FastifyRequest } from \"fastify\";\nimport * as fs from \"fs\";\nimport http from \"http\";\nimport https from \"https\";\nimport mime from \"mime-types\";\nimport { tmpdir } from \"os\";\nimport path from \"path\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { FileService } from \"../../services/file.service.js\";\nimport { getErrors } from \"../../utils/errors.js\";\n\nexport class FilesController {\n  constructor(private fileService: FileService) {}\n\n  private validatePath(filePath: string): boolean {\n    if (path.isAbsolute(filePath)) {\n      return false;\n    }\n\n    if (filePath.includes(\"..\")) {\n      return false;\n    }\n\n    if (filePath.includes(\"\\0\")) {\n      return false;\n    }\n\n    const normalized = path.normalize(filePath);\n    if (normalized.startsWith(\"..\")) {\n      return false;\n    }\n\n    return true;\n  }\n\n  async handleFileUpload(\n    server: FastifyInstance,\n    request: FastifyRequest<{ Params: { sessionId: string } }>,\n    reply: FastifyReply,\n  ) {\n    let tempFilePath: string | null = null;\n\n    try {\n      if (!request.isMultipart()) {\n        return reply.code(400).send({\n          success: false,\n          message: \"Request must be multipart/form-data\",\n        });\n      }\n\n      let filePath: string | null = null;\n      let fileUrl: string | null = null;\n      let fileProvided: boolean = false;\n      let saveFileResult: Awaited<ReturnType<typeof this.fileService.saveFile>> | null = null;\n\n      for await (const part of request.parts()) {\n        if (part.fieldname === \"file\") {\n          if (part.type === \"file\") {\n            const file = part as MultipartFile;\n            fileProvided = true;\n\n            tempFilePath = path.join(tmpdir(), `upload_${uuidv4()}`);\n\n            const writeStream = fs.createWriteStream(tempFilePath);\n            await pipeline(file.file, writeStream);\n          } else if (part.type === \"field\" && typeof part.value === \"string\") {\n            fileUrl = part.value;\n          }\n        } else if (\n          part.fieldname === \"path\" &&\n          part.type === \"field\" &&\n          typeof part.value === \"string\"\n        ) {\n          filePath = part.value;\n        }\n      }\n\n      if (!fileProvided && !fileUrl) {\n        return reply.code(400).send({\n          success: false,\n          message:\n            \"No file provided in the multipart request. The 'file' field must contain either a file or a URL string.\",\n        });\n      }\n\n      let finalPath: string;\n\n      if (fileProvided && tempFilePath) {\n        if (!filePath) {\n          finalPath = randomUUID();\n        } else {\n          if (!this.validatePath(filePath)) {\n            await fs.promises.unlink(tempFilePath).catch(() => {});\n            return reply.code(400).send({\n              success: false,\n              message: \"Invalid path provided\",\n            });\n          }\n          finalPath = filePath;\n        }\n\n        const readStream = fs.createReadStream(tempFilePath);\n        saveFileResult = await this.fileService.saveFile({\n          filePath: finalPath,\n          stream: readStream,\n        });\n\n        await fs.promises.unlink(tempFilePath).catch(() => {});\n        tempFilePath = null;\n      } else if (fileUrl) {\n        if (!filePath) {\n          const urlPath = new URL(fileUrl).pathname;\n          const filename = urlPath.split(\"/\").pop() || randomUUID();\n          const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n          finalPath = sanitizedFilename;\n        } else {\n          if (!this.validatePath(filePath)) {\n            return reply.code(400).send({\n              success: false,\n              message: \"Invalid path provided\",\n            });\n          }\n          finalPath = filePath;\n        }\n\n        const { stream } = await this.createStreamFromUrl(fileUrl);\n        saveFileResult = await this.fileService.saveFile({\n          filePath: finalPath,\n          stream,\n        });\n      }\n\n      if (!saveFileResult) {\n        return reply.code(500).send({\n          success: false,\n          message: \"Failed to save file\",\n        });\n      }\n\n      return reply.send({\n        path: saveFileResult.path,\n        size: saveFileResult.size,\n        lastModified: saveFileResult.lastModified,\n      });\n    } catch (e: unknown) {\n      if (tempFilePath) {\n        await fs.promises.unlink(tempFilePath).catch(() => {});\n      }\n      const error = getErrors(e);\n      return reply.code(500).send({ success: false, message: error });\n    }\n  }\n\n  private async createStreamFromUrl(\n    url: string,\n  ): Promise<{ stream: Readable; contentType?: string; name: string }> {\n    return new Promise((resolve, reject) => {\n      const protocol = url.startsWith(\"https\") ? https : http;\n\n      protocol\n        .get(url, (response) => {\n          if (response.statusCode !== 200) {\n            return reject(new Error(`Failed to fetch file: ${response.statusCode}`));\n          }\n\n          const contentType = response.headers[\"content-type\"];\n          const disposition = response.headers[\"content-disposition\"] || \"\";\n          let name: string | null = null;\n\n          const nameMatch = disposition.match(/filename=\"(.+)\"/i);\n\n          if (nameMatch && nameMatch[1]) {\n            name = nameMatch[1];\n          } else {\n            name = url.split(\"/\").pop() || \"downloaded-file\";\n          }\n\n          resolve({\n            stream: response,\n            contentType,\n            name,\n          });\n        })\n        .on(\"error\", reject);\n    });\n  }\n\n  async handleFileDownload(\n    server: FastifyInstance,\n    request: FastifyRequest<{ Params: { sessionId: string; \"*\": string } }>,\n    reply: FastifyReply,\n  ) {\n    try {\n      const { stream, size, lastModified } = await this.fileService.downloadFile({\n        filePath: request.params[\"*\"],\n      });\n\n      const name = request.params[\"*\"].split(\"/\").pop() || \"downloaded-file\";\n\n      reply\n        .header(\"Content-Type\", mime.lookup(request.params[\"*\"]) || \"application/octet-stream\")\n        .header(\"Content-Length\", size)\n        .header(\"Content-Disposition\", `attachment; filename=\"${encodeURIComponent(name)}\"`)\n        .header(\"Last-Modified\", lastModified.toISOString());\n\n      return reply.send(stream);\n    } catch (e: unknown) {\n      const error = getErrors(e);\n      return reply.code(500).send({ success: false, message: error });\n    }\n  }\n\n  async handleFileHead(\n    server: FastifyInstance,\n    request: FastifyRequest<{ Params: { sessionId: string; \"*\": string } }>,\n    reply: FastifyReply,\n  ) {\n    const { size, lastModified } = await this.fileService.getFile({\n      filePath: request.params[\"*\"],\n    });\n\n    const name = request.params[\"*\"].split(\"/\").pop() || \"downloaded-file\";\n\n    reply\n      .header(\"Content-Length\", size)\n      .header(\"Last-Modified\", lastModified.toISOString())\n      .header(\"Content-Type\", mime.lookup(request.params[\"*\"]) || \"application/octet-stream\")\n      .header(\"Content-Disposition\", `attachment; filename=\"${encodeURIComponent(name)}\"`);\n\n    return reply.code(200).send();\n  }\n\n  async handleFileList(\n    server: FastifyInstance,\n    request: FastifyRequest<{\n      Params: { sessionId: string };\n    }>,\n    reply: FastifyReply,\n  ) {\n    try {\n      const files = await this.fileService.listFiles();\n\n      return reply.send({\n        data: files.map((file) => ({\n          path: file.path,\n          size: file.size,\n          lastModified: file.lastModified,\n        })),\n      });\n    } catch (e: unknown) {\n      const error = getErrors(e);\n      return reply.code(500).send({ success: false, message: error });\n    }\n  }\n\n  async handleFileDelete(\n    server: FastifyInstance,\n    request: FastifyRequest<{ Params: { sessionId: string; \"*\": string } }>,\n    reply: FastifyReply,\n  ) {\n    try {\n      await this.fileService.deleteFile({\n        filePath: request.params[\"*\"],\n      });\n      return reply.code(204).send();\n    } catch (e: unknown) {\n      const error = getErrors(e);\n      return reply.code(500).send({ success: false, message: error });\n    }\n  }\n\n  async handleFileDeleteAll(\n    server: FastifyInstance,\n    request: FastifyRequest<{ Params: { sessionId: string } }>,\n    reply: FastifyReply,\n  ) {\n    try {\n      await this.fileService.cleanupFiles();\n      return reply.code(204).send();\n    } catch (e: unknown) {\n      const error = getErrors(e);\n      return reply.code(500).send({ success: false, message: error });\n    }\n  }\n\n  async handleDownloadArchive(\n    server: FastifyInstance,\n    request: FastifyRequest<{ Params: { sessionId: string } }>,\n    reply: FastifyReply,\n  ) {\n    const prebuiltArchivePath = await this.fileService.getPrebuiltArchivePath();\n\n    try {\n      const stats = await fs.promises.stat(prebuiltArchivePath);\n      if (stats.isFile()) {\n        server.log.info(`Serving prebuilt archive: ${prebuiltArchivePath}`);\n        const stream = fs.createReadStream(prebuiltArchivePath);\n\n        reply.header(\"Content-Type\", \"application/zip\");\n        reply.header(\"Content-Disposition\", `attachment; filename=\"files.zip\"`);\n        reply.header(\"Content-Length\", stats.size);\n        reply.header(\"Last-Modified\", stats.mtime.toUTCString());\n        return reply.send(stream);\n      } else {\n        server.log.warn(`Prebuilt archive path exists but is not a file: ${prebuiltArchivePath}`);\n      }\n\n      server.log.info(\"Sending empty archive.\");\n      reply.header(\"Content-Type\", \"application/zip\");\n      reply.header(\"Content-Disposition\", `attachment; filename=\"files-archive-empty.zip\"`);\n      const emptyArchive = archiver(\"zip\", { zlib: { level: 9 } });\n\n      emptyArchive.pipe(reply.raw);\n\n      await emptyArchive.finalize();\n      return;\n    } catch (err: any) {\n      server.log.error({ err }, \"Error during handleFileArchive\");\n      if (!reply.sent) {\n        try {\n          reply.code(500).send({ message: \"Failed to process archive request\" });\n        } catch (sendError: unknown) {\n          server.log.error(\n            { err: sendError },\n            \"Error sending 500 response after archive handling error\",\n          );\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "api/src/modules/files/files.routes.ts",
    "content": "import fastifyMultipart from \"@fastify/multipart\";\nimport { FastifyInstance, FastifyRequest } from \"fastify\";\nimport { $ref } from \"../../plugins/schemas.js\";\nimport { FileService } from \"../../services/file.service.js\";\nimport { MB } from \"../../utils/size.js\";\nimport { FilesController } from \"./files.controller.js\";\n\nasync function routes(server: FastifyInstance) {\n  const filesController = new FilesController(FileService.getInstance());\n\n  await server.register(fastifyMultipart, {\n    limits: {\n      fileSize: server.steelBrowserConfig.fileStorage?.maxSizePerSession ?? 100 * MB,\n    },\n    attachFieldsToBody: false,\n  });\n\n  server.post(\n    \"/sessions/:sessionId/files\",\n    {\n      schema: {\n        operationId: \"upload_file\",\n        summary: \"Upload a file\",\n        description:\n          \"Uploads a file to a session via `multipart/form-data` with a `file` field that accepts either binary data or a URL string to download from, and an optional `path` field for the file storage path.\",\n        tags: [\"Files\"],\n        consumes: [\"multipart/form-data\"],\n        body: $ref(\"FileUploadRequest\"),\n        response: {\n          200: $ref(\"FileDetails\"),\n        },\n      },\n      validatorCompiler: () => (value) => ({ value }),\n    },\n    async (\n      request: FastifyRequest<{\n        Params: { sessionId: string };\n      }>,\n      reply,\n    ) => filesController.handleFileUpload(server, request, reply),\n  );\n\n  server.head(\n    \"/sessions/:sessionId/files/*\",\n    {\n      schema: {\n        operationId: \"head_file\",\n        summary: \"Head a file\",\n        description: \"Head a file from a session\",\n        tags: [\"Files\"],\n      },\n    },\n    async (request: FastifyRequest<{ Params: { sessionId: string; \"*\": string } }>, reply) =>\n      filesController.handleFileHead(server, request, reply),\n  );\n\n  server.get(\n    \"/sessions/:sessionId/files/*\",\n    {\n      schema: {\n        operationId: \"download_file\",\n        summary: \"Download a file\",\n        description: \"Download a file from a session\",\n        tags: [\"Files\"],\n      },\n    },\n    async (request: FastifyRequest<{ Params: { sessionId: string; \"*\": string } }>, reply) =>\n      filesController.handleFileDownload(server, request, reply),\n  );\n\n  server.get(\n    \"/sessions/:sessionId/files\",\n    {\n      schema: {\n        operationId: \"list_files\",\n        summary: \"List files\",\n        description: \"List all files from the session in descending order.\",\n        tags: [\"Files\"],\n        response: {\n          200: $ref(\"MultipleFiles\"),\n        },\n      },\n    },\n    async (\n      request: FastifyRequest<{\n        Params: { sessionId: string };\n      }>,\n      reply,\n    ) => filesController.handleFileList(server, request, reply),\n  );\n\n  server.delete(\n    \"/sessions/:sessionId/files/*\",\n    {\n      schema: {\n        operationId: \"delete_file\",\n        summary: \"Delete a file\",\n        description: \"Delete a file from a session\",\n        tags: [\"Files\"],\n        response: {\n          204: {\n            type: \"null\",\n            description: \"No content\",\n          },\n        },\n      },\n    },\n    async (request: FastifyRequest<{ Params: { sessionId: string; \"*\": string } }>, reply) =>\n      filesController.handleFileDelete(server, request, reply),\n  );\n\n  server.delete(\n    \"/sessions/:sessionId/files\",\n    {\n      schema: {\n        operationId: \"delete_all_files\",\n        summary: \"Delete all files\",\n        description: \"Delete all files from a session\",\n        tags: [\"Files\"],\n        response: {\n          204: {\n            type: \"null\",\n            description: \"No content\",\n          },\n        },\n      },\n    },\n\n    async (request: FastifyRequest<{ Params: { sessionId: string } }>, reply) =>\n      filesController.handleFileDeleteAll(server, request, reply),\n  );\n\n  server.get(\n    \"/sessions/:sessionId/files.zip\",\n    {\n      schema: {\n        operationId: \"download_archive\",\n        summary: \"Download archive\",\n        description: \"Download all files from the session as a zip archive.\",\n        tags: [\"Files\"],\n      },\n    },\n    async (request: FastifyRequest<{ Params: { sessionId: string } }>, reply) =>\n      filesController.handleDownloadArchive(server, request, reply),\n  );\n}\n\nexport default routes;\n"
  },
  {
    "path": "api/src/modules/files/files.schema.ts",
    "content": "import { z } from \"zod\";\n\nconst FileUploadRequest = z.object({\n  file: z.any().describe(\"The file to upload (binary) or URL string to download from\"),\n  path: z.string().optional().describe(\"Path to the file in the storage system\"),\n});\n\nconst FileDetails = z.object({\n  path: z.string().describe(\"Path to the file in the storage system\"),\n  size: z.number().describe(\"Size of the file in bytes\"),\n  lastModified: z.string().datetime().describe(\"Timestamp when the file was last updated\"),\n});\n\nconst MultipleFiles = z.object({\n  data: z.array(FileDetails).describe(\"Array of files for the current page\"),\n});\n\nexport type FileDetails = z.infer<typeof FileDetails>;\nexport type MultipleFiles = z.infer<typeof MultipleFiles>;\nexport type FileUploadRequest = z.infer<typeof FileUploadRequest>;\n\nexport const filesSchemas = {\n  FileUploadRequest,\n  FileDetails,\n  MultipleFiles,\n};\n\nexport default filesSchemas;\n"
  },
  {
    "path": "api/src/modules/logs/logs.routes.ts",
    "content": "import { FastifyPluginAsync, FastifyRequest } from \"fastify\";\nimport { LogQuerySchema, ExportLogsSchema, LogQueryInput } from \"./logs.schema.js\";\nimport { randomUUID } from \"crypto\";\nimport { $ref } from \"../../plugins/schemas.js\";\nimport { LogQuery } from \"../../services/cdp/instrumentation/storage/index.js\";\nimport { EmitEvent } from \"../../types/enums.js\";\n\nconst logsRoutes: FastifyPluginAsync = async (fastify) => {\n  const storage = fastify.cdpService.getInstrumentationLogger()?.getStorage?.();\n\n  if (!storage) {\n    fastify.log.warn(\"Log storage not available. Logs routes will not work.\");\n    return;\n  }\n\n  /**\n   * Query logs from local storage\n   */\n  fastify.get(\n    \"/query\",\n    {\n      schema: {\n        querystring: $ref(\"LogQuerySchema\"),\n        tags: [\"Logs\"],\n        description: \"Query browser logs from local storage\",\n      },\n    },\n    async (request: FastifyRequest<{ Querystring: LogQueryInput }>) => {\n      const query = request.query;\n\n      let result;\n      try {\n        result = await storage.query({\n          startTime: query.startTime ? new Date(query.startTime) : undefined,\n          endTime: query.endTime ? new Date(query.endTime) : undefined,\n          eventTypes: query.eventTypes ? query.eventTypes.split(\",\") : undefined,\n          pageId: query.pageId,\n          targetType: query.targetType,\n          limit: query.limit,\n          offset: query.offset,\n        });\n      } catch (error) {\n        fastify.log.error({ error }, \"Error querying logs\");\n        throw error;\n      }\n\n      return result;\n    },\n  );\n\n  /**\n   * Get log statistics\n   */\n  fastify.get(\n    \"/stats\",\n    {\n      schema: {\n        tags: [\"Logs\"],\n        description: \"Get statistics about stored browser logs\",\n      },\n    },\n    async () => {\n      const stats = await storage.getStats();\n\n      return {\n        totalEvents: stats.totalEvents,\n        oldestEvent: stats.oldestEvent?.toISOString() || null,\n        newestEvent: stats.newestEvent?.toISOString() || null,\n        sizeBytes: stats.sizeBytes,\n      };\n    },\n  );\n\n  /**\n   * Stream logs in real-time using Server-Sent Events\n   */\n  fastify.get(\n    \"/stream\",\n    {\n      schema: {\n        tags: [\"Logs\"],\n        description: \"Stream browser logs in real-time using SSE\",\n      },\n    },\n    async (request, reply) => {\n      const logger = fastify.cdpService.getInstrumentationLogger();\n\n      if (!logger) {\n        return reply.code(503).send({ error: \"Browser logger not available\" });\n      }\n\n      // Set SSE headers\n      reply.raw.writeHead(200, {\n        \"Content-Type\": \"text/event-stream\",\n        \"Cache-Control\": \"no-cache\",\n        Connection: \"keep-alive\",\n      });\n\n      // Send initial comment to establish connection\n      reply.raw.write(\": connected\\n\\n\");\n\n      // Listen for new log events\n      const handleLog = (event: any, context: any) => {\n        const data = JSON.stringify({ ...event, ...context });\n        reply.raw.write(`data: ${data}\\n\\n`);\n      };\n\n      logger.on?.(EmitEvent.Log, handleLog);\n\n      // Clean up on disconnect\n      request.raw.on(\"close\", () => {\n        logger.off?.(EmitEvent.Log, handleLog);\n      });\n    },\n  );\n\n  /**\n   * Export logs to Parquet format\n   */\n  fastify.post(\n    \"/export\",\n    {\n      schema: {\n        querystring: $ref(\"LogQuerySchema\"),\n        tags: [\"Logs\"],\n        description: \"Export browser logs to Parquet format\",\n      },\n    },\n    async (request: FastifyRequest<{ Querystring: LogQuery }>) => {\n      const query = request.query;\n\n      // Generate a unique filename\n      const fileName = `steel-browser-logs-${randomUUID()}.parquet`;\n      const filePath = `/tmp/steel-browser-exports/${fileName}`;\n\n      // Export with optional query filters\n      const exportedPath = await storage.exportToParquet(filePath, query);\n\n      return {\n        filePath: exportedPath,\n        message: \"Logs exported successfully\",\n      };\n    },\n  );\n\n  /**\n   * Clear all logs from storage\n   */\n  fastify.delete(\n    \"/\",\n    {\n      schema: {\n        tags: [\"Logs\"],\n        description: \"Clear all browser logs from storage\",\n      },\n    },\n    async () => {\n      await storage.clear();\n\n      return {\n        message: \"Logs cleared successfully\",\n      };\n    },\n  );\n};\n\nexport default logsRoutes;\n"
  },
  {
    "path": "api/src/modules/logs/logs.schema.ts",
    "content": "import { z } from \"zod\";\n\nexport const LogQuerySchema = z.object({\n  startTime: z.string().datetime().optional(),\n  endTime: z.string().datetime().optional(),\n  eventTypes: z.string().optional(),\n  pageId: z.string().optional(),\n  targetType: z.string().optional(),\n  limit: z.number().int().min(1).max(1000).optional().default(100),\n  offset: z.number().int().min(0).optional().default(0),\n});\n\nexport const LogStatsSchema = z.object({\n  totalEvents: z.number(),\n  oldestEvent: z.string().datetime().nullable(),\n  newestEvent: z.string().datetime().nullable(),\n  sizeBytes: z.number(),\n});\n\nexport const LogQueryResultSchema = z.object({\n  events: z.array(z.record(z.any())),\n  total: z.number(),\n  hasMore: z.boolean(),\n});\n\nexport const ExportLogsSchema = z.object({\n  query: LogQuerySchema.optional(),\n});\n\nexport type LogQueryInput = z.infer<typeof LogQuerySchema>;\nexport type LogStatsOutput = z.infer<typeof LogStatsSchema>;\nexport type LogQueryResultOutput = z.infer<typeof LogQueryResultSchema>;\nexport type ExportLogsInput = z.infer<typeof ExportLogsSchema>;\n\nexport const loggingSchemas = {\n  LogQuerySchema,\n  LogStatsSchema,\n  LogQueryResultSchema,\n  ExportLogsSchema,\n};\n\nexport default loggingSchemas;\n"
  },
  {
    "path": "api/src/modules/selenium/selenium.routes.ts",
    "content": "import { FastifyInstance, FastifyReply, FastifyRequest } from \"fastify\";\nimport fastifyReplyFrom from \"@fastify/reply-from\";\n\nasync function routes(server: FastifyInstance) {\n  server.register(fastifyReplyFrom, {\n    base: server.seleniumService.getSeleniumServerUrl(),\n  });\n\n  server.all(\n    \"/selenium/wd/*\",\n    { schema: { hide: true } },\n    async (request: FastifyRequest, reply: FastifyReply) => {\n      if (request.url === \"/selenium/wd/session\" && request.method === \"POST\") {\n        const body = request.body as any;\n        if (!body.capabilities) {\n          body.capabilities = {};\n        }\n        if (!body.capabilities.alwaysMatch) {\n          body.capabilities.alwaysMatch = {};\n        }\n        if (!body.capabilities.alwaysMatch[\"goog:chromeOptions\"]) {\n          body.capabilities.alwaysMatch[\"goog:chromeOptions\"] = {};\n        }\n        if (!body.capabilities.alwaysMatch[\"goog:chromeOptions\"].args) {\n          body.capabilities.alwaysMatch[\"goog:chromeOptions\"].args = [];\n        }\n        const chromeArgs = await server.seleniumService.getChromeArgs();\n        body.capabilities.alwaysMatch[\"goog:chromeOptions\"].args.push(...chromeArgs);\n        request.body = body;\n\n        return reply.from(\"/session\", {\n          body,\n          rewriteHeaders(headers, request) {\n            headers[\"content-type\"] = \"application/json; charset=utf-8\";\n            headers[\"accept\"] = \"application/json; charset=utf-8\";\n            return headers;\n          },\n          rewriteRequestHeaders(request, headers) {\n            headers[\"content-type\"] = \"application/json; charset=utf-8\";\n            headers[\"accept\"] = \"application/json; charset=utf-8\";\n            return headers;\n          },\n        });\n      }\n      return reply.from(request.url.replace(\"/selenium/wd\", \"\"), {\n        body: request.body,\n        rewriteRequestHeaders(request, headers) {\n          headers[\"content-type\"] = \"application/json; charset=utf-8\";\n          headers[\"accept\"] = \"application/json; charset=utf-8\";\n          return headers;\n        },\n        rewriteHeaders(headers, request) {\n          headers[\"content-type\"] = \"application/json; charset=utf-8\";\n          headers[\"accept\"] = \"application/json; charset=utf-8\";\n          return headers;\n        },\n      });\n    },\n  );\n}\n\nexport default routes;\n"
  },
  {
    "path": "api/src/modules/selenium/selenium.schema.ts",
    "content": "import { z } from \"zod\";\n\nconst LaunchRequest = z.object({\n  options: z.object({\n    args: z.array(z.string()).optional(),\n    chromiumSandbox: z.boolean().optional(),\n    devtools: z.boolean().optional(),\n    downloadsPath: z.string().optional(),\n    headless: z.boolean().optional(),\n    ignoreDefaultArgs: z.union([z.boolean(), z.array(z.string())]).optional(),\n    proxyUrl: z.string().optional(),\n    timeout: z.number().optional(),\n    tracesDir: z.string().optional(),\n  }),\n  req: z.any().optional(),\n  stealth: z.boolean().optional(),\n  cookies: z.array(z.any()).optional(),\n  userAgent: z.string().optional(),\n  extensions: z.array(z.string()).optional(),\n  logSinkUrl: z.string().optional().describe(\"Deprecated\"),\n  customHeaders: z.record(z.string()).optional(),\n  timezone: z.string().optional(),\n  dimensions: z\n    .object({\n      width: z.number(),\n      height: z.number(),\n    })\n    .nullable()\n    .optional(),\n});\n\nconst LaunchResponse = z.object({\n  success: z.boolean(),\n});\n\nexport type LaunchRequest = z.infer<typeof LaunchRequest>;\n\nexport const seleniumSchemas = {\n  LaunchRequest,\n  LaunchResponse,\n};\n\nexport default seleniumSchemas;\n"
  },
  {
    "path": "api/src/modules/sessions/sessions.controller.ts",
    "content": "import { CDPService } from \"../../services/cdp/cdp.service.js\";\nimport { FastifyInstance, FastifyReply, FastifyRequest } from \"fastify\";\nimport { getErrors } from \"../../utils/errors.js\";\nimport { CreateSessionRequest, SessionDetails, SessionStreamRequest } from \"./sessions.schema.js\";\nimport { CookieData } from \"../../services/context/types.js\";\nimport { getUrl, getBaseUrl } from \"../../utils/url.js\";\n\nexport const handleLaunchBrowserSession = async (\n  server: FastifyInstance,\n  request: CreateSessionRequest,\n  reply: FastifyReply,\n) => {\n  try {\n    const {\n      sessionId,\n      proxyUrl,\n      userDataDir,\n      persist,\n      userAgent,\n      sessionContext,\n      extensions,\n      logSinkUrl,\n      timezone,\n      dimensions,\n      isSelenium,\n      blockAds,\n      optimizeBandwidth,\n      extra,\n      credentials,\n      skipFingerprintInjection,\n      userPreferences,\n      deviceConfig,\n      headless,\n    } = request.body;\n\n    return await server.sessionService.startSession({\n      sessionId,\n      proxyUrl,\n      userDataDir,\n      persist,\n      userAgent,\n      sessionContext: sessionContext as {\n        cookies?: CookieData[] | undefined;\n        localStorage?: Record<string, Record<string, any>> | undefined;\n      },\n      extensions,\n      logSinkUrl,\n      timezone,\n      dimensions,\n      isSelenium,\n      blockAds,\n      optimizeBandwidth,\n      extra,\n      credentials,\n      skipFingerprintInjection,\n      userPreferences,\n      deviceConfig,\n      headless,\n    });\n  } catch (e: unknown) {\n    server.log.error({ err: e }, \"Failed lauching browser session\");\n    const error = getErrors(e);\n    return reply.code(500).send({ success: false, message: error });\n  }\n};\n\nexport const handleExitBrowserSession = async (\n  server: FastifyInstance,\n  request: FastifyRequest,\n  reply: FastifyReply,\n) => {\n  try {\n    const sessionDetails = await server.sessionService.endSession();\n\n    reply.send({ success: true, ...sessionDetails });\n  } catch (e: any) {\n    const error = getErrors(e);\n    return reply.code(500).send({ success: false, message: error });\n  }\n};\n\nexport const handleGetBrowserContext = async (\n  browserService: CDPService,\n  request: FastifyRequest,\n  reply: FastifyReply,\n) => {\n  const context = await browserService.getBrowserState();\n  return reply.send(context);\n};\n\nexport const handleGetSessionDetails = async (\n  server: FastifyInstance,\n  request: FastifyRequest<{ Params: { sessionId: string } }>,\n  reply: FastifyReply,\n) => {\n  const sessionId = request.params.sessionId;\n  if (sessionId !== server.sessionService.activeSession.id) {\n    return reply.send({\n      id: sessionId,\n      createdAt: new Date().toISOString(),\n      status: \"released\",\n      duration: 0,\n      eventCount: 0,\n      timeout: 0,\n      creditsUsed: 0,\n      websocketUrl: getBaseUrl(\"ws\"),\n      debugUrl: getUrl(\"v1/sessions/debug\"),\n      debuggerUrl: getUrl(\"v1/devtools/inspector.html\"),\n      sessionViewerUrl: getBaseUrl(),\n      userAgent: \"\",\n      isSelenium: false,\n      proxy: \"\",\n      proxyTxBytes: 0,\n      proxyRxBytes: 0,\n      solveCaptcha: false,\n    } as SessionDetails);\n  }\n\n  const session = server.sessionService.activeSession;\n  const duration = new Date().getTime() - new Date(session.createdAt).getTime();\n  console.log(\"duration\", duration);\n  return reply.send({\n    ...session,\n    duration,\n  });\n};\n\nexport const handleGetSessions = async (\n  server: FastifyInstance,\n  request: FastifyRequest,\n  reply: FastifyReply,\n) => {\n  const currentSession = {\n    ...server.sessionService.activeSession,\n    duration:\n      new Date().getTime() - new Date(server.sessionService.activeSession.createdAt).getTime(),\n  };\n  const pastSessions = server.sessionService.pastSessions;\n  return reply.send({ sessions: [currentSession, ...pastSessions] });\n};\n\nexport const handleGetSessionStream = async (\n  server: FastifyInstance,\n  request: SessionStreamRequest,\n  reply: FastifyReply,\n) => {\n  const { showControls, theme, interactive, pageId, pageIndex } = request.query;\n\n  const singlePageMode = !!(pageId || pageIndex);\n\n  // Construct WebSocket URL with page parameters if present\n  let wsUrl = getUrl(\"v1/sessions/cast\", \"ws\");\n  if (pageId) {\n    wsUrl += `?pageId=${encodeURIComponent(pageId)}`;\n  } else if (pageIndex) {\n    wsUrl += `?pageIndex=${encodeURIComponent(pageIndex)}`;\n  }\n\n  return reply.view(\"live-session-streamer.ejs\", {\n    wsUrl,\n    showControls,\n    theme,\n    interactive,\n    dimensions: server.sessionService.activeSession.dimensions,\n    singlePageMode,\n  });\n};\n\nexport const handleGetSessionLiveDetails = async (\n  server: FastifyInstance,\n  request: FastifyRequest<{ Params: { id: string } }>,\n  reply: FastifyReply,\n) => {\n  try {\n    const pages = await server.cdpService.getAllPages();\n\n    const pagesInfo = await Promise.all(\n      pages.map(async (page) => {\n        try {\n          const pageId = page.target()._targetId;\n\n          const title = await page.title();\n\n          let favicon: string | null = null;\n          try {\n            favicon = await page.evaluate(() => {\n              const iconLink = document.querySelector(\n                'link[rel=\"icon\"], link[rel=\"shortcut icon\"]',\n              );\n              if (iconLink) {\n                const href = iconLink.getAttribute(\"href\");\n                if (href?.startsWith(\"http\")) return href;\n                if (href?.startsWith(\"//\")) return window.location.protocol + href;\n                if (href?.startsWith(\"/\")) return window.location.origin + href;\n                return window.location.origin + \"/\" + href;\n              }\n              return null;\n            });\n          } catch (error) {}\n\n          return {\n            id: pageId,\n            url: page.url(),\n            title,\n            favicon,\n          };\n        } catch (error) {\n          console.error(\"Error collecting page info:\", error);\n          return null;\n        }\n      }),\n    );\n\n    const validPagesInfo = pagesInfo.filter((page) => page !== null);\n\n    const browserVersion = await server.cdpService.getBrowserState();\n\n    const browserState = {\n      status: server.sessionService.activeSession.status,\n      userAgent: server.sessionService.activeSession.userAgent,\n      browserVersion,\n      initialDimensions: server.sessionService.activeSession.dimensions || {\n        width: 1920,\n        height: 1080,\n      },\n      pageCount: validPagesInfo.length,\n    };\n\n    return reply.send({\n      pages: validPagesInfo,\n      browserState,\n      websocketUrl: server.sessionService.activeSession.websocketUrl,\n      sessionViewerUrl: server.sessionService.activeSession.sessionViewerUrl,\n      sessionViewerFullscreenUrl: `${server.sessionService.activeSession.sessionViewerUrl}?showControls=false`,\n    });\n  } catch (error) {\n    console.error(\"Error getting session state:\", error);\n    return reply.code(500).send({\n      message: \"Failed to get session state\",\n      error: getErrors(error),\n    });\n  }\n};\n"
  },
  {
    "path": "api/src/modules/sessions/sessions.routes.ts",
    "content": "import { FastifyInstance, FastifyReply, FastifyRequest } from \"fastify\";\nimport {\n  handleLaunchBrowserSession,\n  handleGetBrowserContext,\n  handleExitBrowserSession,\n  handleGetSessionDetails,\n  handleGetSessions,\n  handleGetSessionStream,\n  handleGetSessionLiveDetails,\n} from \"./sessions.controller.js\";\nimport { handleScrape, handleScreenshot, handlePDF } from \"../actions/actions.controller.js\";\nimport { $ref } from \"../../plugins/schemas.js\";\nimport {\n  CreateSessionRequest,\n  RecordedEvents,\n  SessionStreamRequest,\n  SessionsScrapeRequest,\n  SessionsScreenshotRequest,\n  SessionsPDFRequest,\n} from \"./sessions.schema.js\";\nimport { BrowserEventType, EmitEvent } from \"../../types/enums.js\";\n\nasync function routes(server: FastifyInstance) {\n  server.get(\n    \"/health\",\n    {\n      schema: {\n        operationId: \"health\",\n        description: \"Check if the server and browser are running\",\n        tags: [\"Health\"],\n        summary: \"Check if the server and browser are running\",\n      },\n    },\n    async (request: FastifyRequest, reply: FastifyReply) => {\n      if (!server.cdpService.isRunning()) {\n        return reply.status(503).send({ status: \"service_unavailable\" });\n      }\n      return reply.send({ status: \"ok\" });\n    },\n  );\n  server.post(\n    \"/sessions\",\n    {\n      schema: {\n        operationId: \"launch_browser_session\",\n        description: \"Launch a browser session\",\n        tags: [\"Sessions\"],\n        summary: \"Launch a browser session\",\n        body: $ref(\"CreateSession\"),\n        response: {\n          200: $ref(\"SessionDetails\"),\n        },\n      },\n    },\n    async (request: CreateSessionRequest, reply: FastifyReply) =>\n      handleLaunchBrowserSession(server, request, reply),\n  );\n\n  server.get(\n    \"/sessions\",\n    {\n      schema: {\n        operationId: \"get_sessions\",\n        description: \"Get all sessions\",\n        tags: [\"Sessions\"],\n        summary: \"Get all sessions\",\n        response: {\n          200: $ref(\"MultipleSessions\"),\n        },\n      },\n    },\n    async (request: FastifyRequest, reply: FastifyReply) =>\n      handleGetSessions(server, request, reply),\n  );\n\n  server.get(\n    \"/sessions/:sessionId\",\n    {\n      schema: {\n        operationId: \"get_session_details\",\n        description: \"Get session details\",\n        tags: [\"Sessions\"],\n        summary: \"Get session details\",\n        response: {\n          200: $ref(\"SessionDetails\"),\n        },\n      },\n    },\n    async (request: FastifyRequest<{ Params: { sessionId: string } }>, reply: FastifyReply) =>\n      handleGetSessionDetails(server, request, reply),\n  );\n\n  server.get(\n    \"/sessions/:sessionId/context\",\n    {\n      schema: {\n        operationId: \"get_browser_context\",\n        description: \"Get a browser context\",\n        tags: [\"Sessions\"],\n        summary: \"Get a browser context\",\n        response: {\n          200: $ref(\"SessionContextSchema\"),\n        },\n      },\n    },\n    async (request: FastifyRequest, reply: FastifyReply) =>\n      handleGetBrowserContext(server.cdpService, request, reply),\n  );\n\n  server.post(\n    \"/sessions/:sessionId/release\",\n    {\n      schema: {\n        operationId: \"release_browser_session\",\n        description: \"Release a browser session\",\n        tags: [\"Sessions\"],\n        summary: \"Release a browser session\",\n        response: {\n          200: $ref(\"ReleaseSession\"),\n        },\n      },\n    },\n    async (request: FastifyRequest, reply: FastifyReply) =>\n      handleExitBrowserSession(server, request, reply),\n  );\n\n  server.post(\n    \"/sessions/release\",\n    {\n      schema: {\n        operationId: \"release_browser_sessions\",\n        description: \"Release browser sessions\",\n        tags: [\"Sessions\"],\n        summary: \"Release browser sessions\",\n        response: {\n          200: $ref(\"ReleaseSession\"),\n        },\n      },\n    },\n    async (request: FastifyRequest, reply: FastifyReply) =>\n      handleExitBrowserSession(server, request, reply),\n  );\n\n  server.get(\n    \"/sessions/debug\",\n    {\n      onRequest: [],\n      schema: {\n        operationId: \"get_session_debugger_stream\",\n        description: \"Returns an HTML page with a live debugger view of the session\",\n        tags: [\"Sessions\"],\n        summary: \"Get session debugger view\",\n        querystring: $ref(\"SessionStreamQuery\"),\n        response: {\n          200: $ref(\"SessionStreamResponse\"),\n        },\n      },\n    },\n    async (request: SessionStreamRequest, reply: FastifyReply) =>\n      handleGetSessionStream(server, request, reply),\n  );\n\n  server.post(\n    \"/events\",\n    {\n      schema: {\n        operationId: \"receive_events\",\n        description: \"Receive recorded events from the browser\",\n        tags: [\"Sessions\"],\n        summary: \"Receive recorded events from the browser\",\n        body: $ref(\"RecordedEvents\"),\n      },\n    },\n    async (request: FastifyRequest<{ Body: RecordedEvents }>, reply: FastifyReply) => {\n      server.cdpService.getInstrumentationLogger().record({\n        type: BrowserEventType.Recording,\n        timestamp: new Date().toISOString(),\n        data: request.body,\n      });\n      return reply.send({ status: \"ok\" });\n    },\n  );\n\n  server.get(\n    \"/sessions/:id/live-details\",\n    {\n      onRequest: [],\n      schema: {\n        operationId: \"get_session_live_details\",\n        description:\n          \"Returns the live state of the session, including pages, tabs, and browser state\",\n        tags: [\"Sessions\"],\n        summary: \"Get session live details\",\n        response: {\n          200: $ref(\"SessionLiveDetailsResponse\"),\n        },\n      },\n    },\n    async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) =>\n      handleGetSessionLiveDetails(server, request, reply),\n  );\n\n  server.post(\n    \"/sessions/scrape\",\n    {\n      schema: {\n        operationId: \"scrape_session\",\n        description: \"Scrape Current Session\",\n        tags: [\"Sessions\"],\n        summary: \"Scrape Current Session\",\n        body: $ref(\"ScrapeRequest\"),\n        response: {\n          200: $ref(\"ScrapeResponse\"),\n        },\n      },\n    },\n    async (request: SessionsScrapeRequest, reply: FastifyReply) =>\n      handleScrape(server.sessionService, server.cdpService, request, reply),\n  );\n\n  server.post(\n    \"/sessions/screenshot\",\n    {\n      schema: {\n        operationId: \"screenshot_session\",\n        description: \"Take Screenshot of Current Session\",\n        tags: [\"Sessions\"],\n        summary: \"Take Screenshot of Current Session\",\n        body: $ref(\"ScreenshotRequest\"),\n        response: {\n          200: $ref(\"ScreenshotResponse\"),\n        },\n      },\n    },\n    async (request: SessionsScreenshotRequest, reply: FastifyReply) =>\n      handleScreenshot(server.sessionService, server.cdpService, request, reply),\n  );\n\n  server.post(\n    \"/sessions/pdf\",\n    {\n      schema: {\n        operationId: \"pdf_session\",\n        description: \"Generate PDF of Current Session\",\n        tags: [\"Sessions\"],\n        summary: \"Generate PDF of Current Session\",\n        body: $ref(\"PDFRequest\"),\n        response: {\n          200: $ref(\"PDFResponse\"),\n        },\n      },\n    },\n    async (request: SessionsPDFRequest, reply: FastifyReply) =>\n      handlePDF(server.sessionService, server.cdpService, request, reply),\n  );\n}\n\nexport default routes;\n"
  },
  {
    "path": "api/src/modules/sessions/sessions.schema.ts",
    "content": "import { FastifyRequest } from \"fastify\";\nimport { z } from \"zod\";\nimport {\n  ScrapeRequestBody,\n  ScreenshotRequestBody,\n  PDFRequestBody,\n} from \"../actions/actions.schema.js\";\nimport { SessionContextSchema } from \"../../services/context/types.js\";\n\nexport type CredentialsOptions = z.infer<typeof SessionCredentials>;\nexport const SessionCredentials = z\n  .object({\n    autoSubmit: z.union([z.boolean(), z.never()]),\n    blurFields: z.union([z.boolean(), z.never()]),\n    exactOrigin: z.union([z.boolean(), z.never()]),\n  })\n  .partial()\n  .optional()\n  .describe(\"Configuration for session credentials\");\n\nconst CreateSession = z.object({\n  sessionId: z.string().uuid().optional().describe(\"Unique identifier for the session\"),\n  proxyUrl: z.string().optional().describe(\"Proxy URL to use for the session\"),\n  userAgent: z.string().optional().describe(\"User agent string to use for the session\"),\n  sessionContext: SessionContextSchema.optional().describe(\n    \"Session context data to be used in the created session\",\n  ),\n  isSelenium: z.boolean().optional().describe(\"Indicates if Selenium is used in the session\"),\n  blockAds: z\n    .boolean()\n    .optional()\n    .describe(\"Flag to indicate if ads should be blocked in the session\"),\n  optimizeBandwidth: z\n    .union([\n      z.boolean(),\n      z\n        .object({\n          blockImages: z.boolean().optional(),\n          blockMedia: z.boolean().optional(),\n          blockStylesheets: z.boolean().optional(),\n          blockHosts: z.array(z.string()).optional(),\n          blockUrlPatterns: z.array(z.string()).optional(),\n        })\n        .strict(),\n    ])\n    .optional()\n    .describe(\n      \"Enable bandwidth optimizations. Passing true enables all flags (except hosts/patterns). Object allows granular control.\",\n    ),\n  skipFingerprintInjection: z\n    .boolean()\n    .optional()\n    .describe(\"Flag to indicate if fingerprint injection should be skipped for this session.\"),\n  deviceConfig: z\n    .object({\n      device: z.enum([\"desktop\", \"mobile\"]).default(\"desktop\"),\n    })\n    .optional()\n    .describe(\n      \"Device configuration for the session. Specify 'mobile' for mobile device fingerprints and configurations.\",\n    ),\n  // Specific to hosted steel\n  logSinkUrl: z.string().optional().describe(\"Deprecated: Log sink URL to use for the session\"),\n  extensions: z.array(z.string()).optional().describe(\"Extensions to use for the session\"),\n  persist: z.boolean().optional().describe(\"Flag to indicate if session should be persisted\"),\n  userDataDir: z.string().optional().describe(\"User data directory path to use for the session\"),\n  timezone: z.string().optional().describe(\"Timezone to use for the session\"),\n  dimensions: z\n    .object({\n      width: z.number(),\n      height: z.number(),\n    })\n    .optional()\n    .describe(\"Dimensions to use for the session\"),\n  userPreferences: z\n    .record(z.string(), z.any())\n    .optional()\n    .describe(\n      \"Chrome user preferences to customize browser behavior (e.g., font size, popup blocking, notification settings)\",\n    ),\n  extra: z\n    .record(z.string(), z.any())\n    .optional()\n    .describe(\"Extra metadata to help initialize the session\"),\n  credentials: SessionCredentials,\n  headless: z.boolean().optional().describe(\"Headless mode for the session\"),\n});\n\nconst SessionDetails = z.object({\n  id: z.string().uuid().describe(\"Unique identifier for the session\"),\n  createdAt: z.string().datetime().describe(\"Timestamp when the session started\"),\n  status: z.enum([\"idle\", \"live\", \"released\", \"failed\"]).describe(\"Status of the session\"),\n  duration: z.number().int().describe(\"Duration of the session in milliseconds\"),\n  eventCount: z.number().int().describe(\"Number of events processed in the session\"),\n  dimensions: z\n    .object({\n      width: z.number(),\n      height: z.number(),\n    })\n    .optional()\n    .describe(\"Dimensions used for the session\"),\n  timeout: z.number().int().describe(\"Session timeout duration in milliseconds\"),\n  creditsUsed: z.number().int().describe(\"Amount of credits consumed by the session\"),\n  websocketUrl: z.string().describe(\"URL for the session's WebSocket connection\"),\n  debugUrl: z.string().describe(\"URL for a viewing the live browser instance for the session\"),\n  debuggerUrl: z.string().describe(\"URL for debugging the session\"),\n  sessionViewerUrl: z.string().describe(\"URL to view session details\"),\n  userAgent: z.string().optional().describe(\"User agent string used in the session\"),\n  proxy: z.string().optional().describe(\"Proxy server used for the session\"),\n  proxyTxBytes: z\n    .number()\n    .int()\n    .nonnegative()\n    .describe(\"Amount of data transmitted through the proxy\"),\n  proxyRxBytes: z\n    .number()\n    .int()\n    .nonnegative()\n    .describe(\"Amount of data received through the proxy\"),\n  solveCaptcha: z.boolean().optional().describe(\"Indicates if captcha solving is enabled\"),\n  isSelenium: z.boolean().optional().describe(\"Indicates if Selenium is used in the session\"),\n});\n\nconst ReleaseSession = SessionDetails.merge(\n  z.object({ success: z.boolean().describe(\"Indicates if the session was successfully released\") }),\n);\n\nconst RecordedEvents = z.object({\n  events: z.array(z.any()).describe(\"Events to emit\"),\n});\n\nconst SessionStreamQuery = z.object({\n  showControls: z\n    .boolean()\n    .optional()\n    .default(true)\n    .describe(\"Show controls in the browser iframe\"),\n  theme: z\n    .enum([\"dark\", \"light\"])\n    .optional()\n    .default(\"dark\")\n    .describe(\"Theme of the browser iframe\"),\n  interactive: z.boolean().optional().default(true).describe(\"Make the browser iframe interactive\"),\n  pageId: z.string().optional().describe(\"Page ID to connect to\"),\n  pageIndex: z.string().optional().describe(\"Page index (or tab index) to connect to\"),\n});\n\nconst SessionLiveDetailsResponse = z.object({\n  sessionViewerUrl: z.string(),\n  sessionViewerFullscreenUrl: z.string(),\n  websocketUrl: z.string(),\n  pages: z.array(\n    z.object({\n      id: z.string(),\n      url: z.string(),\n      title: z.string(),\n      favicon: z.string().nullable(),\n    }),\n  ),\n  browserState: z.object({\n    status: z.enum([\"idle\", \"live\", \"released\", \"failed\"]),\n    userAgent: z.string(),\n    browserVersion: z.string(),\n    initialDimensions: z.object({\n      width: z.number(),\n      height: z.number(),\n    }),\n    pageCount: z.number(),\n  }),\n});\n\nconst SessionStreamResponse = z.string().describe(\"HTML content for the session streamer view\");\n\nconst MultipleSessions = z.object({\n  sessions: z.array(SessionDetails),\n});\n\nexport type SessionsScrapeRequestBody = Omit<ScrapeRequestBody, \"url\">;\nexport type SessionsScrapeRequest = FastifyRequest<{ Body: SessionsScrapeRequestBody }>;\n\nexport type SessionsScreenshotRequestBody = Omit<ScreenshotRequestBody, \"url\">;\nexport type SessionsScreenshotRequest = FastifyRequest<{ Body: SessionsScreenshotRequestBody }>;\n\nexport type SessionsPDFRequestBody = Omit<PDFRequestBody, \"url\">;\nexport type SessionsPDFRequest = FastifyRequest<{ Body: SessionsPDFRequestBody }>;\n\nexport type RecordedEvents = z.infer<typeof RecordedEvents>;\nexport type CreateSessionBody = z.infer<typeof CreateSession>;\nexport type CreateSessionRequest = FastifyRequest<{ Body: CreateSessionBody }>;\nexport type SessionDetails = z.infer<typeof SessionDetails>;\nexport type MultipleSessions = z.infer<typeof MultipleSessions>;\n\nexport type SessionStreamQuery = z.infer<typeof SessionStreamQuery>;\nexport type SessionStreamRequest = FastifyRequest<{ Querystring: SessionStreamQuery }>;\n\nexport const browserSchemas = {\n  CreateSession,\n  SessionDetails,\n  MultipleSessions,\n  SessionContextSchema,\n  RecordedEvents,\n  ReleaseSession,\n  SessionStreamQuery,\n  SessionStreamResponse,\n  SessionLiveDetailsResponse,\n};\n\nexport default browserSchemas;\n"
  },
  {
    "path": "api/src/plugins/browser-session.ts",
    "content": "import { FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport { SessionService } from \"../services/session.service.js\";\n\nconst browserSessionPlugin: FastifyPluginAsync = async (fastify, _options) => {\n  const sessionService = new SessionService({\n    cdpService: fastify.cdpService,\n    seleniumService: fastify.seleniumService,\n    fileService: fastify.fileService,\n    logger: fastify.log,\n  });\n  fastify.decorate(\"sessionService\", sessionService);\n};\n\nexport default fp(browserSessionPlugin, \"5.x\");\n"
  },
  {
    "path": "api/src/plugins/browser-socket/browser-socket.ts",
    "content": "import { type FastifyInstance, type FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport { WebSocketServer } from \"ws\";\nimport { WebSocketRegistryService } from \"../../services/websocket-registry.service.js\";\nimport { WebSocketHandler, WebSocketHandlerContext } from \"../../types/websocket.js\";\nimport { defaultHandlers } from \"./handlers/index.js\";\n\nexport interface BrowserSocketOptions {\n  customHandlers?: WebSocketHandler[];\n}\n\n// WebSocket server instance\nconst wss = new WebSocketServer({ noServer: true });\n\nconst browserWebSocket: FastifyPluginAsync<BrowserSocketOptions> = async (\n  fastify: FastifyInstance,\n  options: BrowserSocketOptions,\n) => {\n  if (!fastify.cdpService.isRunning()) {\n    fastify.log.info(\"Launching browser...\");\n    await fastify.cdpService.launch();\n    fastify.log.info(\"Browser launched successfully\");\n  }\n\n  const registry = new WebSocketRegistryService();\n\n  defaultHandlers.forEach((handler) => {\n    registry.registerHandler(handler);\n  });\n\n  if (options.customHandlers) {\n    options.customHandlers.forEach((handler) => {\n      registry.registerHandler(handler);\n    });\n  }\n\n  fastify.decorate(\"webSocketRegistry\", registry);\n\n  fastify.server.on(\"upgrade\", async (request, socket, head) => {\n    fastify.log.info(\"Upgrading browser socket...\");\n    const url = request.url ?? \"\";\n    const params = Object.fromEntries(\n      new URL(url || \"\", `http://${request.headers.host}`).searchParams.entries(),\n    );\n\n    const context: WebSocketHandlerContext = {\n      fastify,\n      wss,\n      params,\n    };\n\n    const handler = registry.matchHandler(url);\n\n    if (handler) {\n      try {\n        await handler.handler(request, socket, head, context);\n      } catch (err) {\n        fastify.log.error({ err }, `WebSocket handler error for ${url}`);\n        socket.destroy();\n      }\n    } else {\n      fastify.log.info(\"Connecting to CDP...\");\n      try {\n        await fastify.cdpService.proxyWebSocket(request, socket, head);\n      } catch (err) {\n        fastify.log.error({ err }, \"CDP WebSocket error\");\n        socket.destroy();\n      }\n    }\n  });\n};\n\nexport default fp(browserWebSocket, { name: \"browser-websocket\" });\n"
  },
  {
    "path": "api/src/plugins/browser-socket/casting.handler.ts",
    "content": "import { IncomingMessage } from \"http\";\nimport puppeteer, { Browser, CDPSession, Page } from \"puppeteer-core\";\nimport { Duplex } from \"stream\";\nimport WebSocket, { Server } from \"ws\";\n\nimport { env } from \"../../env.js\";\nimport { SessionService } from \"../../services/session.service.js\";\nimport {\n  CloseTabEvent,\n  GetSelectedTextEvent,\n  KeyEvent,\n  MouseEvent,\n  NavigationEvent,\n  PageInfo,\n} from \"../../types/casting.js\";\nimport { getPageFavicon, getPageTitle, navigatePage } from \"../../utils/casting.js\";\n\nexport async function handleCastSession(\n  request: IncomingMessage,\n  socket: Duplex,\n  head: Buffer,\n  wss: Server,\n  sessionService: SessionService,\n  params: Record<string, string> | undefined,\n): Promise<void> {\n  const id = request.url?.split(\"/sessions/\")[1].split(\"/cast\")[0];\n\n  if (!id) {\n    console.error(\"Cast Session ID not found\");\n    socket.destroy();\n    return;\n  }\n\n  const session = await sessionService.activeSession;\n  if (!session) {\n    console.error(`Cast Session ${id} not found`);\n    socket.destroy();\n    return;\n  }\n\n  const queryParams = new URLSearchParams(request.url?.split(\"?\")[1] || \"\");\n  const requestedPageId = params?.pageId || queryParams.get(\"pageId\") || null;\n  const requestedPageIndex = params?.pageIndex || queryParams.get(\"pageIndex\") || null;\n\n  const tabDiscoveryMode =\n    queryParams.get(\"tabInfo\") === \"true\" || (!requestedPageId && !requestedPageIndex);\n\n  const { height, width } = (session.dimensions as { width: number; height: number }) ?? {\n    width: 1920,\n    height: 1080,\n  };\n\n  wss.handleUpgrade(request, socket, head, async (ws) => {\n    let browser: Browser | null = null;\n    let targetPage: Page | null = null;\n    let targetClient: CDPSession | null = null;\n    let targetPageId: string | null = null;\n\n    const activePages = new Map<string, Page>();\n\n    let heartbeatInterval: NodeJS.Timeout | null = null;\n\n    const handleSessionCleanup = () => {\n      if (heartbeatInterval) {\n        clearInterval(heartbeatInterval);\n        heartbeatInterval = null;\n      }\n\n      if (targetPage) {\n        targetPage.removeAllListeners(\"framenavigated\");\n      }\n\n      // Clean up screencast\n      if (targetClient) {\n        try {\n          targetClient.send(\"Page.stopScreencast\").catch((err) => {\n            // Ignore errors about closed targets\n            if (!err.message?.includes(\"Target closed\")) {\n              console.error(\"Error stopping screencast:\", err);\n            }\n          });\n\n          targetClient.detach().catch((err) => {\n            // Ignore errors about closed targets\n            if (!err.message?.includes(\"Target closed\")) {\n              console.error(\"Error detaching client:\", err);\n            }\n          });\n\n          targetClient = null;\n        } catch (err) {\n          console.error(\"Error during screencast cleanup:\", err);\n        }\n      }\n\n      // Disconnect browser\n      if (browser) {\n        try {\n          browser.disconnect().catch((err) => {\n            console.error(\"Error disconnecting browser:\", err);\n          });\n          browser = null;\n        } catch (err) {\n          console.error(\"Error during browser disconnect:\", err);\n        }\n      }\n\n      // Force garbage collection if available (Node.js with --expose-gc flag)\n      if (global.gc) {\n        try {\n          global.gc();\n        } catch (err) {\n          console.error(\"Error during garbage collection:\", err);\n        }\n      }\n    };\n\n    const sendTabList = async () => {\n      try {\n        if (ws.readyState !== WebSocket.OPEN || !tabDiscoveryMode) return;\n\n        const tabList: PageInfo[] = [];\n\n        for (const [pageId, page] of activePages.entries()) {\n          tabList.push({\n            id: pageId,\n            url: page.url(),\n            title: await getPageTitle(page),\n            favicon: await getPageFavicon(page),\n          });\n        }\n\n        ws.send(\n          JSON.stringify({\n            type: \"tabList\",\n            tabs: tabList,\n            firstTabId: tabList.length > 0 ? tabList[0].id : null,\n          }),\n        );\n      } catch (error) {\n        console.error(\"Error sending tab list:\", error);\n      }\n    };\n\n    const findTargetPage = async (\n      pages: Page[],\n    ): Promise<{ page: Page; pageId: string } | null> => {\n      if (tabDiscoveryMode) return null; // No target page in tab discovery mode\n\n      if (requestedPageId) {\n        for (const page of pages) {\n          try {\n            //@ts-expect-error\n            const pageId = page.target()._targetId;\n            if (pageId === requestedPageId) {\n              return { page, pageId };\n            }\n          } catch (err) {\n            console.error(\"Error accessing page target ID:\", err);\n          }\n        }\n      } else if (requestedPageIndex) {\n        const index = parseInt(requestedPageIndex, 10);\n        if (index >= 0 && index < pages.length) {\n          const page = pages[index];\n          //@ts-expect-error\n          const pageId = page.target()._targetId;\n          return { page, pageId };\n        }\n      }\n\n      return null;\n    };\n\n    try {\n      browser = await puppeteer.connect({\n        browserWSEndpoint: `ws://${env.HOST}:${env.PORT}`,\n      });\n\n      if (!browser) {\n        console.error(\"Failed to connect to browser\");\n        socket.destroy();\n        return;\n      }\n\n      const pages = await browser.pages();\n\n      if (tabDiscoveryMode) {\n        for (const page of pages) {\n          //@ts-expect-error\n          const pageId = page.target()._targetId;\n          activePages.set(pageId, page);\n        }\n\n        // Initial tab list\n        await sendTabList();\n\n        // Setup page creation/deletion tracking\n        browser.on(\"targetcreated\", async (target) => {\n          if (target.type() === \"page\") {\n            try {\n              const page = await target.asPage();\n              //@ts-expect-error\n              const pageId = target._targetId;\n              activePages.set(pageId, page);\n              await sendTabList();\n            } catch (err) {\n              console.error(\"Error handling new target:\", err);\n            }\n          }\n        });\n\n        browser.on(\"targetdestroyed\", async (target) => {\n          if (target.type() === \"page\") {\n            try {\n              //@ts-expect-error\n              const pageId = target._targetId;\n              if (activePages.has(pageId)) {\n                activePages.delete(pageId);\n\n                if (ws.readyState === WebSocket.OPEN) {\n                  ws.send(\n                    JSON.stringify({\n                      type: \"tabClosed\",\n                      pageId,\n                    }),\n                  );\n\n                  await sendTabList();\n                }\n              }\n            } catch (err) {\n              console.error(\"Error handling destroyed target:\", err);\n            }\n          }\n        });\n\n        // Setup heartbeat to detect dead connections\n        heartbeatInterval = setInterval(() => {\n          if (ws.readyState === WebSocket.OPEN) {\n            try {\n              ws.ping();\n            } catch (err) {\n              console.error(\"Error sending ping:\", err);\n              handleSessionCleanup();\n            }\n          } else {\n            handleSessionCleanup();\n          }\n        }, 30000);\n\n        ws.on(\"close\", () => {\n          handleSessionCleanup();\n        });\n\n        ws.on(\"error\", (err) => {\n          console.error(\"Tab discovery WebSocket error:\", err);\n          handleSessionCleanup();\n        });\n\n        return;\n      } else {\n        const targetResult = await findTargetPage(pages);\n\n        if (!targetResult) {\n          console.error(\n            `Target page not found for ${\n              requestedPageId ? `pageId=${requestedPageId}` : `pageIndex=${requestedPageIndex}`\n            }`,\n          );\n          socket.destroy();\n          return;\n        }\n\n        targetPage = targetResult.page;\n        targetPageId = targetResult.pageId;\n\n        await targetPage.bringToFront();\n\n        // Setup screencast for the target page\n        targetClient = await targetPage.target().createCDPSession();\n\n        ws.on(\"message\", async (message) => {\n          try {\n            const data:\n              | MouseEvent\n              | KeyEvent\n              | NavigationEvent\n              | CloseTabEvent\n              | GetSelectedTextEvent = JSON.parse(message.toString());\n            const { type } = data;\n\n            if (!targetClient || !targetPage) {\n              console.error(\"No target page or client available for input handling\");\n              return;\n            }\n\n            switch (type) {\n              case \"mouseEvent\": {\n                const { event } = data as MouseEvent;\n                await targetClient.send(\"Input.dispatchMouseEvent\", {\n                  type: event.type,\n                  x: event.x,\n                  y: event.y,\n                  button: event.button,\n                  buttons: event.button === \"none\" ? 0 : 1,\n                  clickCount: event.clickCount || 1,\n                  modifiers: event.modifiers || 0,\n                  deltaX: event.deltaX,\n                  deltaY: event.deltaY,\n                });\n                break;\n              }\n              case \"keyEvent\": {\n                const { event } = data as KeyEvent;\n                await targetClient.send(\"Input.dispatchKeyEvent\", {\n                  type: event.type,\n                  text: event.text,\n                  unmodifiedText: event.text ? event.text.toLowerCase() : undefined,\n                  code: event.code,\n                  key: event.key,\n                  windowsVirtualKeyCode: event.keyCode,\n                  nativeVirtualKeyCode: event.keyCode,\n                  modifiers: event.modifiers || 0,\n                  autoRepeat: false,\n                  isKeypad: false,\n                  isSystemKey: false,\n                });\n                break;\n              }\n              case \"navigation\": {\n                const { event } = data as NavigationEvent;\n                await navigatePage(event, targetPage);\n                break;\n              }\n              case \"closeTab\": {\n                const { pageId } = data as CloseTabEvent;\n                await targetPage?.close();\n                if (activePages.has(pageId)) {\n                  activePages.delete(pageId);\n                }\n                break;\n              }\n              case \"getSelectedText\": {\n                try {\n                  const selectedText = await targetPage.evaluate(() => {\n                    const selection = window.getSelection();\n                    return selection ? selection.toString() : \"\";\n                  });\n\n                  // Send the selected text back to the client\n                  ws.send(\n                    JSON.stringify({\n                      type: \"selectedTextResponse\",\n                      pageId: (data as GetSelectedTextEvent).pageId,\n                      text: selectedText,\n                    }),\n                  );\n                } catch (error) {\n                  console.error(\"Failed to get selected text:\", error);\n                  ws.send(\n                    JSON.stringify({\n                      type: \"selectedTextResponse\",\n                      pageId: (data as GetSelectedTextEvent).pageId,\n                      text: \"\",\n                      error: error instanceof Error ? error.message : \"Unknown error\",\n                    }),\n                  );\n                }\n                break;\n              }\n\n              default:\n                console.warn(\"Unknown event type:\", type);\n            }\n          } catch (err) {\n            console.error(\"Error handling WebSocket message:\", err);\n          }\n        });\n\n        // Setup device metrics and start screencast\n        await targetClient.send(\"Page.setDeviceMetricsOverride\", {\n          screenHeight: height,\n          screenWidth: width,\n          width,\n          height,\n          mobile: false,\n          screenOrientation: { angle: 90, type: \"landscapePrimary\" },\n          deviceScaleFactor: 1,\n        });\n\n        await targetClient.send(\"Page.startScreencast\", {\n          format: \"jpeg\",\n          quality: 75,\n          maxWidth: width,\n          maxHeight: height,\n        });\n\n        // Handle screencast frames\n        targetClient.on(\"Page.screencastFrame\", async ({ data, sessionId }) => {\n          try {\n            // Acknowledge the frame right away to free up memory\n            await targetClient?.send(\"Page.screencastFrameAck\", { sessionId });\n\n            if (ws.readyState === WebSocket.OPEN) {\n              // Get page metadata\n              const title = await getPageTitle(targetPage!);\n              const favicon = await getPageFavicon(targetPage!);\n\n              // Send frame data\n              ws.send(\n                JSON.stringify({\n                  pageId: targetPageId,\n                  url: targetPage?.url(),\n                  title,\n                  favicon,\n                  data,\n                }),\n              );\n            }\n          } catch (err) {\n            console.error(\"Error in Page.screencastFrame handler:\", err);\n          }\n        });\n\n        // Cleanup when target is destroyed\n        browser.on(\"targetdestroyed\", async (target) => {\n          if (target.type() === \"page\") {\n            try {\n              //@ts-expect-error\n              const pageId = target._targetId;\n\n              if (pageId === targetPageId) {\n                if (ws.readyState === WebSocket.OPEN) {\n                  ws.send(\n                    JSON.stringify({\n                      type: \"targetClosed\",\n                      pageId: targetPageId,\n                    }),\n                  );\n                }\n\n                // Cleanup and close connection\n                handleSessionCleanup();\n                ws.close();\n              }\n            } catch (err) {\n              console.error(\"Error handling destroyed target:\", err);\n            }\n          }\n        });\n\n        // Setup heartbeat to detect dead connections\n        heartbeatInterval = setInterval(() => {\n          if (ws.readyState === WebSocket.OPEN) {\n            try {\n              ws.ping();\n            } catch (err) {\n              console.error(\"Error sending ping:\", err);\n              handleSessionCleanup();\n            }\n          } else {\n            handleSessionCleanup();\n          }\n        }, 30000);\n\n        // Cleanup on WebSocket closure\n        ws.on(\"close\", () => {\n          handleSessionCleanup();\n        });\n\n        // Handle errors\n        ws.on(\"error\", (err) => {\n          console.error(\"Cast WebSocket error:\", err);\n          handleSessionCleanup();\n        });\n      }\n    } catch (err) {\n      console.error(\"Error in cast session:\", err);\n      handleSessionCleanup();\n      socket.destroy();\n    }\n  });\n}\n"
  },
  {
    "path": "api/src/plugins/browser-socket/handlers/cast.handler.ts",
    "content": "import { IncomingMessage } from \"http\";\nimport { Duplex } from \"stream\";\nimport { WebSocketHandler, WebSocketHandlerContext } from \"../../../types/websocket.js\";\nimport { handleCastSession } from \"../casting.handler.js\";\n\nexport const castHandler: WebSocketHandler = {\n  path: \"/v1/sessions/cast\",\n  handler: async (\n    request: IncomingMessage,\n    socket: Duplex,\n    head: Buffer,\n    context: WebSocketHandlerContext,\n  ) => {\n    context.fastify.log.info(\"Connecting to cast...\");\n    await handleCastSession(\n      request,\n      socket,\n      head,\n      context.wss,\n      context.fastify.sessionService,\n      context.params,\n    );\n  },\n};\n"
  },
  {
    "path": "api/src/plugins/browser-socket/handlers/index.ts",
    "content": "export { logsHandler } from \"./logs.handler.js\";\nexport { castHandler } from \"./cast.handler.js\";\nexport { pageIdHandler } from \"./pageId.handler.js\";\nexport { recordingHandler } from \"./recording.handler.js\";\n\nimport { WebSocketHandler } from \"../../../types/websocket.js\";\nimport { logsHandler } from \"./logs.handler.js\";\nimport { castHandler } from \"./cast.handler.js\";\nimport { pageIdHandler } from \"./pageId.handler.js\";\nimport { recordingHandler } from \"./recording.handler.js\";\n\nexport const defaultHandlers: WebSocketHandler[] = [\n  logsHandler,\n  castHandler,\n  pageIdHandler,\n  recordingHandler,\n];\n"
  },
  {
    "path": "api/src/plugins/browser-socket/handlers/logs.handler.ts",
    "content": "import { IncomingMessage } from \"http\";\nimport { Duplex } from \"stream\";\nimport { WebSocket } from \"ws\";\nimport { EmitEvent } from \"../../../types/enums.js\";\nimport { WebSocketHandler, WebSocketHandlerContext } from \"../../../types/websocket.js\";\n\nfunction handleLogsWebSocket(context: WebSocketHandlerContext, ws: WebSocket) {\n  const { fastify } = context;\n\n  const messageHandler = (payload: { pageId: string }) => {\n    if (ws.readyState === WebSocket.OPEN) {\n      ws.send(JSON.stringify([payload]));\n    }\n  };\n\n  fastify.cdpService.on(EmitEvent.Log, messageHandler);\n\n  ws.on(\"error\", (err) => {\n    fastify.log.error({ err }, \"Logs WebSocket error\");\n  });\n\n  ws.on(\"close\", () => {\n    fastify.log.info(\"Logs WebSocket connection closed\");\n    fastify.cdpService.removeListener(EmitEvent.Log, messageHandler);\n  });\n}\n\nexport const logsHandler: WebSocketHandler = {\n  path: \"/v1/sessions/logs\",\n  handler: (\n    request: IncomingMessage,\n    socket: Duplex,\n    head: Buffer,\n    context: WebSocketHandlerContext,\n  ) => {\n    context.fastify.log.info(\"Connecting to logs...\");\n    context.wss.handleUpgrade(request, socket, head, (ws) => handleLogsWebSocket(context, ws));\n  },\n};\n"
  },
  {
    "path": "api/src/plugins/browser-socket/handlers/pageId.handler.ts",
    "content": "import { IncomingMessage } from \"http\";\nimport { Duplex } from \"stream\";\nimport { WebSocket } from \"ws\";\nimport { WebSocketHandler, WebSocketHandlerContext } from \"../../../types/websocket.js\";\n\nfunction handlePageIdWebSocket(context: WebSocketHandlerContext, ws: WebSocket) {\n  const { fastify } = context;\n\n  const messageHandler = (payload: { pageId: string }) => {\n    if (ws.readyState === WebSocket.OPEN) {\n      ws.send(JSON.stringify(payload));\n    }\n  };\n\n  fastify.cdpService.on(\"pageId\", messageHandler);\n\n  ws.on(\"error\", (err) => {\n    fastify.log.error({ err }, \"PageId WebSocket error\");\n  });\n\n  ws.on(\"close\", () => {\n    fastify.log.info(\"PageId WebSocket connection closed\");\n    fastify.cdpService.removeListener(\"pageId\", messageHandler);\n  });\n}\n\nexport const pageIdHandler: WebSocketHandler = {\n  path: \"/v1/sessions/pageId\",\n  handler: (\n    request: IncomingMessage,\n    socket: Duplex,\n    head: Buffer,\n    context: WebSocketHandlerContext,\n  ) => {\n    context.fastify.log.info(\"Connecting to pageId...\");\n    context.wss.handleUpgrade(request, socket, head, (ws) => handlePageIdWebSocket(context, ws));\n  },\n};\n"
  },
  {
    "path": "api/src/plugins/browser-socket/handlers/recording.handler.ts",
    "content": "import { IncomingMessage } from \"http\";\nimport { Duplex } from \"stream\";\nimport { WebSocket } from \"ws\";\nimport { EmitEvent } from \"../../../types/enums.js\";\nimport { WebSocketHandler, WebSocketHandlerContext } from \"../../../types/websocket.js\";\n\nfunction handleRecordingWebSocket(context: WebSocketHandlerContext, ws: WebSocket) {\n  const { fastify } = context;\n\n  const messageHandler = (payload: { events: Record<string, any>[] }) => {\n    if (ws.readyState === WebSocket.OPEN) {\n      ws.send(JSON.stringify(payload.events));\n    }\n  };\n\n  fastify.cdpService.on(EmitEvent.Recording, messageHandler);\n\n  // TODO: handle inputs to browser from client\n  ws.on(\"message\", async (message) => {});\n\n  ws.on(\"close\", () => {\n    fastify.log.info(\"Recording WebSocket connection closed\");\n    fastify.cdpService.removeListener(EmitEvent.Recording, messageHandler);\n  });\n\n  ws.on(\"error\", (err) => {\n    fastify.log.error({ err }, \"Recording WebSocket error\");\n  });\n}\n\nexport const recordingHandler: WebSocketHandler = {\n  path: \"/v1/sessions/recording\",\n  handler: (\n    request: IncomingMessage,\n    socket: Duplex,\n    head: Buffer,\n    context: WebSocketHandlerContext,\n  ) => {\n    context.fastify.log.info(\"Connecting to recording events...\");\n    context.wss.handleUpgrade(request, socket, head, (ws) => handleRecordingWebSocket(context, ws));\n  },\n};\n"
  },
  {
    "path": "api/src/plugins/browser.ts",
    "content": "import { FastifyPluginAsync } from \"fastify\";\nimport { CDPService } from \"../services/cdp/cdp.service.js\";\nimport fp from \"fastify-plugin\";\nimport { BrowserLauncherOptions } from \"../types/index.js\";\nimport {\n  DuckDBStorage,\n  InMemoryStorage,\n  LogStorage,\n} from \"../services/cdp/instrumentation/storage/index.js\";\nimport path from \"path\";\nimport os from \"os\";\nimport { env } from \"../env.js\";\n\ndeclare module \"fastify\" {\n  interface FastifyInstance {\n    cdpService: CDPService;\n    registerCDPLaunchHook: (hook: (config: BrowserLauncherOptions) => Promise<void> | void) => void;\n    registerCDPShutdownHook: (\n      hook: (config: BrowserLauncherOptions | null) => Promise<void> | void,\n    ) => void;\n  }\n}\n\nconst browserInstancePlugin: FastifyPluginAsync = async (fastify, _options) => {\n  const loggingConfig = fastify.steelBrowserConfig?.logging || {};\n  const enableStorage = loggingConfig.enableStorage ?? env.LOG_STORAGE_ENABLED ?? false;\n  const enableConsoleLogging = loggingConfig.enableConsoleLogging ?? true;\n\n  let storage: LogStorage | null = null;\n  if (enableStorage) {\n    const storagePath =\n      loggingConfig.storagePath ||\n      env.LOG_STORAGE_PATH ||\n      path.join(os.tmpdir(), \"steel-browser-logs\", \"logs.duckdb\");\n\n    storage = new DuckDBStorage({\n      dbPath: storagePath,\n      maxThreads: 1,\n      memoryLimit: \"128MB\",\n      parquetCompression: \"none\",\n      enableWriteBuffer: true,\n      writeBufferSize: 200,\n      writeBufferFlushInterval: 2000,\n    });\n\n    await storage.initialize();\n    fastify.log.info(`Log storage initialized at ${storagePath}`);\n  } else {\n    // Use in-memory storage for development\n    storage = new InMemoryStorage(1000);\n    await storage.initialize();\n    fastify.log.info(\"Using in-memory log storage\");\n  }\n\n  const cdpService = new CDPService({}, fastify.log, storage, enableConsoleLogging);\n\n  fastify.decorate(\"cdpService\", cdpService);\n  fastify.decorate(\n    \"registerCDPLaunchHook\",\n    (hook: (config: BrowserLauncherOptions) => Promise<void> | void) => {\n      cdpService.registerLaunchHook(hook);\n    },\n  );\n  fastify.decorate(\n    \"registerCDPShutdownHook\",\n    (hook: (config: BrowserLauncherOptions | null) => Promise<void> | void) => {\n      cdpService.registerShutdownHook(hook);\n    },\n  );\n\n  fastify.addHook(\"onListen\", async function () {\n    this.log.info(\"Launching default browser...\");\n    await cdpService.launch();\n  });\n};\n\nexport default fp(browserInstancePlugin, \"5.x\");\n"
  },
  {
    "path": "api/src/plugins/custom-body-parser.ts",
    "content": "import fp from \"fastify-plugin\";\nimport { type FastifyInstance, type FastifyPluginAsync } from \"fastify\";\n\nconst customBodyParser: FastifyPluginAsync = async (fastify: FastifyInstance) => {\n  fastify.addContentTypeParser(\n    \"application/json\",\n    { parseAs: \"buffer\" },\n    function (req, body, done) {\n      try {\n        switch (true) {\n          case req.url.includes(\"/release\"):\n            // Skip parsing for release endpoints\n            done(null, null);\n            break;\n\n          default:\n            // Parse JSON for all other requests\n            done(null, JSON.parse(body.toString()));\n        }\n      } catch (error) {\n        done(error as Error, undefined);\n      }\n    },\n  );\n};\n\nexport default fp(customBodyParser, { name: \"custom-body-parser\" });\n"
  },
  {
    "path": "api/src/plugins/file-storage.ts",
    "content": "import { FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport { FileService } from \"../services/file.service.js\";\n\nconst fileStoragePlugin: FastifyPluginAsync = async (fastify, _options) => {\n  fastify.log.info(\"Registering file service\");\n  fastify.decorate(\"fileService\", FileService.getInstance());\n};\n\nexport default fp(fileStoragePlugin, \"5.x\");\n"
  },
  {
    "path": "api/src/plugins/request-logger.ts",
    "content": "import { FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\n\ndeclare module \"fastify\" {\n  interface FastifyReply {\n    startTime: number;\n  }\n}\n\n// https://github.com/fastify/fastify/blob/main/lib/logger.js#L67\nfunction now() {\n  const ts = process.hrtime();\n  return ts[0] * 1e3 + ts[1] / 1e6;\n}\n\nconst logger: FastifyPluginAsync = async (fastify) => {\n  fastify.addHook(\"onRequest\", (req, reply, done) => {\n    reply.startTime = now();\n    done();\n  });\n\n  fastify.addHook(\"onResponse\", (req, reply, done) => {\n    if (\n      req.method !== \"OPTIONS\" &&\n      req.raw.url !== \"/status\" &&\n      req.raw.url !== \"/v1/events\" &&\n      req.raw.url !== \"/\"\n    ) {\n      req.log.info(\n        {\n          ip: getClientIp(req),\n          url: req.raw.url,\n          method: req.method,\n          statusCode: reply.raw.statusCode,\n          durationMs: roundMS(now() - reply.startTime),\n        },\n        \"request completed\",\n      );\n    }\n    done();\n  });\n};\n\nexport default fp(logger);\n\nfunction roundMS(num: number): number {\n  return Math.trunc(num * 100) / 100;\n}\n\nfunction getClientIp(req: any): string {\n  if (req.headers[\"x-forwarded-for\"]) {\n    return req.headers[\"x-forwarded-for\"].split(\",\")[0];\n  }\n  if (req.headers[\"x-real-ip\"]) {\n    return req.headers[\"x-real-ip\"];\n  }\n  if (req.raw && req.raw.connection) {\n    return req.raw.connection.remoteAddress || \"unknown\";\n  }\n  if (req.raw && req.raw.socket) {\n    return req.raw.socket.remoteAddress || \"unknown\";\n  }\n  if (req.socket) {\n    return req.socket.remoteAddress || \"unknown\";\n  }\n  return \"unknown\";\n}\n"
  },
  {
    "path": "api/src/plugins/scalar-theme.ts",
    "content": "export default `\n/* Basic theme */\n.light-mode {\n  --scalar-color-1: #21201c; /* Sand 12 */\n  --scalar-color-2: #63635e; /* Sand 11 */\n  --scalar-color-3: #82827c; /* Sand 10 */\n  --scalar-color-accent: #1b180f; /* Yellow 9 */\n\n  --scalar-background-1: #fdfdfc; /* Sand 1 */\n  --scalar-background-2: #f9f9f8; /* Sand 2 */\n  --scalar-background-3: #f1f0ef; /* Sand 3 */\n  --scalar-background-accent: #ffe62a1f;\n\n  --scalar-border-color: #f1f0ef; /* Sand 3 */\n  --scalar-code-language-color-supersede: var(--scalar-color-3);\n}\n\n.dark-mode {\n  --scalar-color-1: #eeeeec; /* Sand 12 */\n  --scalar-color-2: #b5b3ad; /* Sand 11 */\n  --scalar-color-3: rgba(180, 179, 173, 0.6); /* Sand 10 with opacity */\n  --scalar-color-accent: #ffe62a; /* Yellow 9 */\n\n  --scalar-background-1: #111110; /* Sand 1 */\n  --scalar-background-2: #191918; /* Sand 2 */\n  --scalar-background-3: #222221; /* Sand 3 */\n  --scalar-background-accent: #ffe62a1f;\n\n  --scalar-border-color: #222221; /* Sand Dark 3 */\n  --scalar-code-language-color-supersede: var(--scalar-color-3);\n}\n\n/* Document Sidebar */\n.light-mode .t-doc__sidebar {\n  --scalar-sidebar-background-1: var(--scalar-background-1);\n  --scalar-sidebar-item-hover-color: currentColor;\n  --scalar-sidebar-item-hover-background: var(--scalar-background-2);\n  --scalar-sidebar-item-active-background: var(--scalar-background-accent);\n  --scalar-sidebar-border-color: 0.5px solid var(--scalar-border-color);\n  --scalar-sidebar-color-1: var(--scalar-color-1);\n  --scalar-sidebar-color-2: var(--scalar-color-2);\n  --scalar-sidebar-color-active: #1b180f;\n  --scalar-sidebar-search-background: rgba(33, 32, 28, 0.05);\n  --scalar-sidebar-search-border-color: 1px solid rgba(33, 32, 28, 0.05);\n  --scalar-sidebar-search-color: var(--scalar-color-3);\n  --scalar-background-2: rgba(33, 32, 28, 0.03);\n}\n\n.dark-mode .t-doc__sidebar {\n  --scalar-sidebar-background-1: var(--scalar-background-1);\n  --scalar-sidebar-item-hover-color: currentColor;\n  --scalar-sidebar-item-hover-background: var(--scalar-background-2);\n  --scalar-sidebar-item-active-background: rgba(238, 238, 236, 0.1);\n  --scalar-sidebar-border-color: 0.5px solid var(--scalar-border-color);\n  --scalar-sidebar-color-1: var(--scalar-color-1);\n  --scalar-sidebar-color-2: var(--scalar-color-2);\n  --scalar-sidebar-color-active: var(--scalar-color-accent);\n  --scalar-sidebar-search-background: rgba(238, 238, 236, 0.1);\n  --scalar-sidebar-search-border-color: 1px solid rgba(238, 238, 236, 0.05);\n  --scalar-sidebar-search-color: var(--scalar-color-3);\n}\n\n/* Advanced */\n.light-mode {\n  --scalar-color-green: #069061;\n  --scalar-color-red: #ef0006;\n  --scalar-color-yellow: #edbe20;\n  --scalar-color-blue: #0082d0;\n  --scalar-color-orange: #fb892c;\n  --scalar-color-purple: #5203d1;\n\n  --scalar-button-1: rgba(33, 32, 28, 1);\n  --scalar-button-1-hover: rgba(33, 32, 28, 0.8);\n  --scalar-button-1-color: rgba(253, 253, 252, 0.9);\n}\n\n.dark-mode {\n  --scalar-color-green: #00b648;\n  --scalar-color-red: #dc1b19;\n  --scalar-color-yellow: #ffc90d;\n  --scalar-color-blue: #4eb3ec;\n  --scalar-color-orange: #ff8d4d;\n  --scalar-color-purple: #b191f9;\n\n  --scalar-button-1: rgba(238, 238, 236, 1);\n  --scalar-button-1-hover: rgba(238, 238, 236, 0.9);\n  --scalar-button-1-color: #111110;\n}\n\n/* Custom Theme */\n.dark-mode h2.t-editor__heading,\n.dark-mode .t-editor__page-title h1,\n.dark-mode h1.section-header,\n.dark-mode .markdown h1,\n.dark-mode .markdown h2,\n.dark-mode .markdown h3,\n.dark-mode .markdown h4,\n.dark-mode .markdown h5,\n.dark-mode .markdown h6 {\n  -webkit-text-fill-color: transparent;\n  background-image: linear-gradient(\n    to right bottom,\n    rgb(238, 238, 236) 30%,\n    rgba(238, 238, 236, 0.38)\n  );\n  -webkit-background-clip: text;\n  background-clip: text;\n}\n\n.sidebar-search {\n  backdrop-filter: blur(12px);\n}\n\n@keyframes headerbackground {\n  from {\n    background: transparent;\n    backdrop-filter: none;\n  }\n  to {\n    background: var(--scalar-header-background-1);\n    backdrop-filter: blur(12px);\n  }\n}\n\n.dark-mode .scalar-card {\n  background: rgba(238, 238, 236, 0.05) !important;\n}\n\n.dark-mode .scalar-card * {\n  --scalar-background-2: transparent !important;\n  --scalar-background-1: transparent !important;\n}\n\n.light-mode .dark-mode.scalar-card *,\n.light-mode .dark-mode.scalar-card {\n  --scalar-background-1: #191918 !important;\n  --scalar-background-2: #191918 !important;\n  --scalar-background-3: #222221 !important;\n}\n\n.light-mode .dark-mode.scalar-card {\n  background: #222221 !important;\n}\n\n.badge {\n  box-shadow: 0 0 0 1px var(--scalar-border-color);\n  margin-right: 6px;\n}\n\n.table-row.required-parameter .table-row-item:nth-of-type(2):after {\n  background: transparent;\n  box-shadow: none;\n}\n\n/* Hero Section Flare */\n.section-flare {\n  width: 100vw;\n  background: radial-gradient(\n    ellipse 80% 50% at 50% -20%,\n    rgba(255, 230, 42, 0.15),\n    transparent\n  );\n  height: 100vh;\n}\n\n/* Document layout */\n.light-mode .t-doc .layout-content,\n.dark-mode .t-doc .layout-content {\n  background: transparent;\n}\n\n.t-doc__header {\n  background: rgba(253, 253, 252, 0.7); /* Sand 1 with opacity for light mode */\n  backdrop-filter: blur(12px);\n  -webkit-backdrop-filter: blur(12px); /* For Safari */\n  border-bottom: 0.5px solid var(--scalar-border-color);\n  position: sticky;\n  top: 0;\n  z-index: 100;\n}\n\n.dark-mode .t-doc__header {\n  background: rgba(17, 17, 16, 0.7); /* Sand 1 dark with opacity */\n}\n/* Sidebar styles */\n.t-doc__sidebar {\n  background: rgba(253, 253, 252, 0.7); /* Sand 1 with opacity for light mode */\n  backdrop-filter: blur(12px);\n  -webkit-backdrop-filter: blur(12px);\n  border-bottom: 0.5px solid var(--scalar-border-color);\n  z-index: 100;\n}\n\n.dark-mode .t-doc__sidebar {\n  background: rgba(17, 17, 16, 0.7) !important;\n}\n`;\n"
  },
  {
    "path": "api/src/plugins/schemas.ts",
    "content": "import { FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport fastifySwagger from \"@fastify/swagger\";\nimport fastifyScalar from \"@scalar/fastify-api-reference\";\nimport { titleCase } from \"../utils/text.js\";\nimport actionSchemas from \"../modules/actions/actions.schema.js\";\nimport cdpSchemas from \"../modules/cdp/cdp.schemas.js\";\nimport logsSchemas from \"../modules/logs/logs.schema.js\";\nimport browserSchemas from \"../modules/sessions/sessions.schema.js\";\nimport seleniumSchemas from \"../modules/selenium/selenium.schema.js\";\nimport scalarTheme from \"./scalar-theme.js\";\nimport { buildJsonSchemas } from \"../utils/schema.js\";\nimport filesSchemas from \"../modules/files/files.schema.js\";\nimport { getBaseUrl } from \"../utils/url.js\";\n\nconst SCHEMAS = {\n  ...actionSchemas,\n  ...browserSchemas,\n  ...logsSchemas,\n  ...cdpSchemas,\n  ...seleniumSchemas,\n  ...filesSchemas,\n};\n\nexport const { schemas, $ref } = buildJsonSchemas(SCHEMAS);\n\nconst schemaPlugin: FastifyPluginAsync = async (fastify) => {\n  for (const schema of schemas) {\n    fastify.addSchema(schema);\n  }\n\n  await fastify.register(fastifySwagger, {\n    openapi: {\n      info: {\n        title: \"Steel Browser Instance API\",\n        description: \"Documentation for controlling a single instance of Steel Browser\",\n        version: \"0.0.1\",\n      },\n      servers: [\n        {\n          url: getBaseUrl(),\n          description: \"Local server\",\n        },\n      ],\n      paths: {}, // paths must be included even if it's an empty object\n      components: {\n        securitySchemes: {},\n      },\n    },\n    refResolver: {\n      buildLocalReference: (json, baseUri, fragment, i) => {\n        return titleCase(json.$id as string) || `Fragment${i}`;\n      },\n    },\n  });\n\n  await fastify.register(fastifyScalar as any, {\n    // scalar still uses fastify v4\n    routePrefix: \"/documentation\",\n    configuration: {\n      customCss: scalarTheme,\n    },\n  });\n};\n\nexport default fp(schemaPlugin);\n"
  },
  {
    "path": "api/src/plugins/selenium.ts",
    "content": "import { FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport { SeleniumService } from \"../services/selenium.service.js\";\n\nconst seleniumPlugin: FastifyPluginAsync = async (fastify, options) => {\n  const seleniumService = new SeleniumService(fastify.log);\n  fastify.decorate(\"seleniumService\", seleniumService);\n};\n\nexport default fp(seleniumPlugin, \"5.x\");\n"
  },
  {
    "path": "api/src/plugins/ui-plugin.ts",
    "content": "import fastifyStatic from \"@fastify/static\";\nimport { FastifyPluginAsync, FastifyRequest, FastifyReply } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\n\nexport interface UIPluginOptions {\n  uiDistPath?: string;\n  uiPrefix?: string;\n}\n\nconst uiPlugin: FastifyPluginAsync<UIPluginOptions> = async (fastify, opts) => {\n  const uiDistPath = opts.uiDistPath || path.join(process.cwd(), \"ui/dist\");\n  const uiPrefix = opts.uiPrefix || \"/ui\";\n\n  if (!fs.existsSync(uiDistPath)) {\n    fastify.log.info(\"UI dist not found, skipping UI serving\");\n    return;\n  }\n\n  fastify.log.info(`UI plugin activated: serving from ${uiDistPath} at ${uiPrefix}`);\n\n  await fastify.register(fastifyStatic, {\n    root: uiDistPath,\n    prefix: uiPrefix,\n    decorateReply: true,\n  });\n\n  fastify.get(\"/\", async (request: FastifyRequest, reply: FastifyReply) => {\n    const userAgent = request.headers[\"user-agent\"];\n\n    // If it's a browser, redirect to UI\n    if (userAgent && userAgent.includes(\"Mozilla\")) {\n      return reply.redirect(uiPrefix);\n    }\n\n    return { message: \"Steel Browser API\", ui: uiPrefix };\n  });\n\n  fastify.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => {\n    const url = request.url;\n    if (url.startsWith(uiPrefix)) {\n      return reply.sendFile(\"index.html\");\n    }\n    return reply.code(404).send({ error: \"Not Found\" });\n  });\n\n  fastify.log.info(\"UI plugin registered successfully\");\n};\n\nexport default fp<UIPluginOptions>(uiPlugin, {\n  name: \"ui-plugin\",\n  fastify: \"5.x\",\n});\n"
  },
  {
    "path": "api/src/routes.ts",
    "content": "export { default as actionsRoutes } from \"./modules/actions/actions.routes.js\";\nexport { default as sessionsRoutes } from \"./modules/sessions/sessions.routes.js\";\nexport { default as seleniumRoutes } from \"./modules/selenium/selenium.routes.js\";\nexport { default as cdpRoutes } from \"./modules/cdp/cdp.routes.js\";\nexport { default as filesRoutes } from \"./modules/files/files.routes.js\";\nexport { default as logsRoutes } from \"./modules/logs/logs.routes.js\";\n"
  },
  {
    "path": "api/src/scripts/fingerprint.js",
    "content": "const _0x28f974 = _0x5610;\n(function (_0x3ccf48, _0x290ea1) {\n  const _0x17ecfd = _0x5610,\n    _0xa8e50e = _0x3ccf48();\n  while (!![]) {\n    try {\n      const _0xb7d3fe =\n        -parseInt(_0x17ecfd(0x155)) / 0x1 +\n        (-parseInt(_0x17ecfd(0x198)) / 0x2) * (-parseInt(_0x17ecfd(0xa3)) / 0x3) +\n        -parseInt(_0x17ecfd(0x10e)) / 0x4 +\n        (-parseInt(_0x17ecfd(0x108)) / 0x5) * (-parseInt(_0x17ecfd(0x182)) / 0x6) +\n        -parseInt(_0x17ecfd(0xe0)) / 0x7 +\n        (-parseInt(_0x17ecfd(0xe1)) / 0x8) * (parseInt(_0x17ecfd(0x99)) / 0x9) +\n        (-parseInt(_0x17ecfd(0xae)) / 0xa) * (-parseInt(_0x17ecfd(0x154)) / 0xb);\n      if (_0xb7d3fe === _0x290ea1) break;\n      else _0xa8e50e[\"push\"](_0xa8e50e[\"shift\"]());\n    } catch (_0x2e18c7) {\n      _0xa8e50e[\"push\"](_0xa8e50e[\"shift\"]());\n    }\n  }\n})(_0x338a, 0x89a89);\nconst originalConsoleDebug = console[_0x28f974(0x113)],\n  originalConsoleLog = console[_0x28f974(0x12f)];\n(console[_0x28f974(0x113)] = function () {}),\n  (console[\"log\"] = function () {\n    const _0x5571e0 = _0x28f974,\n      _0x29b17d = new Error()[_0x5571e0(0x123)] || \"\";\n    if (\n      !(\n        _0x29b17d[_0x5571e0(0x9d)](\"chrome-ext\" + _0x5571e0(0xbe)) ||\n        _0x29b17d[\"includes\"](_0x5571e0(0x175) + \"/\") ||\n        _0x29b17d[_0x5571e0(0x9d)](_0x5571e0(0x105))\n      )\n    )\n      return originalConsoleLog[_0x5571e0(0x9b)](this, arguments);\n  }),\n  delete window[\"cdc_adoQpo\" + _0x28f974(0xb0) + _0x28f974(0x148) + \"ay\"],\n  delete window[_0x28f974(0xf4) + _0x28f974(0xb0) + _0x28f974(0x133) + _0x28f974(0x18b)],\n  delete window[_0x28f974(0xf4) + _0x28f974(0xb0) + _0x28f974(0x9c) + _0x28f974(0x193)];\nfunction _0x5610(_0xa98ee6, _0x384755) {\n  const _0x338afa = _0x338a();\n  return (\n    (_0x5610 = function (_0x561077, _0x486847) {\n      _0x561077 = _0x561077 - 0x99;\n      let _0x5b5815 = _0x338afa[_0x561077];\n      return _0x5b5815;\n    }),\n    _0x5610(_0xa98ee6, _0x384755)\n  );\n}\nconst originalHardwareConcurrency = navigator[_0x28f974(0x10f) + _0x28f974(0x150)],\n  originalDeviceMemory = navigator[_0x28f974(0x164) + \"ry\"] || 0x8;\ndelete navigator[_0x28f974(0x10f) + _0x28f974(0x150)], delete navigator[\"deviceMemo\" + \"ry\"];\nconst originalGetOwnPropertyNames = Object[\"getOwnProp\" + _0x28f974(0xf6)];\nfunction _0x338a() {\n  const _0x499256 = [\n    \"ERSION)\\x20re\",\n    \"getOwnProp\",\n    \"omium)\\x27;\\x20\\x20\",\n    \"Type\\x20===\\x20\\x27\",\n    \"ames(obj);\",\n    \"getSupport\",\n    \"meter\\x20===\\x20\",\n    \"alse,\\x20\\x20\\x20\\x20c\",\n    \"return\\x20pro\",\n    \".\\x20(NVIDIA)\",\n    \"==\\x20ctx.VER\",\n    \"rn\\x20\\x27ANGLE\\x20\",\n    \"sole.log\\x20=\",\n    \"keys\",\n    \"og\\x20=\\x20funct\",\n    \"L\\x20ES\\x20GLSL\\x20\",\n    \"ENDERER_WE\",\n    \"WEBGL_debu\",\n    \"asOwnPrope\",\n    \"\\x20prop.toSt\",\n    \")\\x20return\\x20\\x27\",\n    \"r.hardware\",\n    \"al-webgl\",\n    \"onfigurabl\",\n    \"lse,\\x20\\x20\\x20\\x20co\",\n    \"ameter);\\x20\\x20\",\n    \"igDeviceMe\",\n    \"peof\\x20Offsc\",\n    \"er;\\x20\\x20\\x20\\x20\\x20\\x20c\",\n    \"debug_rend\",\n    \"5465187IyJpyM\",\n    \"20392UGBQMn\",\n    \"1.0\\x20(OpenG\",\n    \"ameter)\\x20{\\x20\",\n    \"\\x20prop\\x20===\\x20\",\n    \"\\x20\\x20\\x20if\\x20(par\",\n    \"BGL\",\n    \"&\\x20prop\\x20!==\",\n    \"ER)\\x20return\",\n    \"e\\x20navigato\",\n    \"LANGUAGE_V\",\n    \"nfigurable\",\n    \"creenCanva\",\n    \"inalOffscr\",\n    \"erties\",\n    \"}\\x20\\x20\\x20\\x20retur\",\n    \"e:\\x20false,\\x20\",\n    \"ole.debug\\x20\",\n    \"turn\\x20\\x27WebG\",\n    \"\\x22\\x20||\\x20\\x20\\x20\\x20\\x20\\x20\",\n    \"cdc_adoQpo\",\n    \"fined\\x27)\\x20{\\x20\",\n    \"ertyNames\",\n    \"reenCanvas\",\n    \"experiment\",\n    \"}});if\\x20(ty\",\n    \"edExtensio\",\n    \"=\\x20function\",\n    \"UNMASKED_R\",\n    \"ion(contex\",\n    \":\\x20false\\x20\\x20}\",\n    \"n/javascri\",\n    \"=\\x20Object.p\",\n    \"ctx.RENDER\",\n    \"return\\x20ori\",\n    \"erer_info\\x27\",\n    \"ter\\x20===\\x20ct\",\n    \"chrome://\",\n    \"ludes(\\x22CDP\",\n    \"igator.dev\",\n    \"307365eYbXvj\",\n    \"s.prototyp\",\n    \"\\x22);\",\n    \"extType\\x20==\",\n    \"Memory\\x22:\\x20{\",\n    \"urrency\\x22\\x20&\",\n    \"4191028ywmhdl\",\n    \"hardwareCo\",\n    \"VERSION\",\n    \"VIDIA,\\x20NVI\",\n    \"\\x20\\x22Runtime\\x22\",\n    \"debug\",\n    \"on(\\x27WEBGL_\",\n    \"mory\\x20=\\x20nav\",\n    \"console.de\",\n    \"Concurrenc\",\n    \"pe.getCont\",\n    \"ring().inc\",\n    \"RENDERER\",\n    \"\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\",\n    \"meter\\x20=\\x20fu\",\n    \"DIA\\x20GeForc\",\n    \"\\x20\\x20\\x20\\x20value:\",\n    \"merable:\\x20f\",\n    \"op)\\x20{\\x20\\x20\\x20if\",\n    \"ctx\\x20=\\x20orig\",\n    \"rce\\x20GTX\\x2010\",\n    \"stack\",\n    \"\\x20{\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\",\n    \"erable:\\x20fa\",\n    \"DOR_WEBGL)\",\n    \"tx.getPara\",\n    \"Google\\x20Inc\",\n    \"pertyNames\",\n    \"\\x20\\x20if\\x20(para\",\n    \"ameter\\x20===\",\n    \".UNMASKED_\",\n    \"RENDERER_W\",\n    \"gInfo)\\x20{\\x20\\x20\",\n    \"log\",\n    \"tType,\\x20att\",\n    \"\\x27;\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\",\n    \"xtType,\\x20at\",\n    \"ZLmcfl_Pro\",\n    \"if\\x20(parame\",\n    \"const\\x20orig\",\n    \"t.prototyp\",\n    \"webgl2\\x27))\\x20\",\n    \"revokeObje\",\n    \"nProperty.\",\n    \"filter\",\n    \"=\\x20\\x27webgl\\x27\\x20\",\n    \"push\",\n    \"tch\\x20(e)\\x20{}\",\n    \"Worker\",\n    \"y;const\\x20or\",\n    \"lGetParame\",\n    \"e.getConte\",\n    \"(this,\\x20par\",\n    \"\\x20const\\x20ori\",\n    \"his,\\x20conte\",\n    \"\\x20!==\\x20\\x27unde\",\n    \"ion()\\x20{};\\x20\",\n    \"nfo\\x20=\\x20ctx.\",\n    \"ZLmcfl_Arr\",\n    \"\\x20\\x20if\\x20(obj\\x20\",\n    \"\\x20return\\x20\\x27G\",\n    \"||\\x20context\",\n    \")\\x20{};\\x20self\",\n    \"opertyName\",\n    \"eenGetCont\",\n    \"(navigator\",\n    \"ncurrency\",\n    \"8,\\x20\\x20\\x20\\x20enum\",\n    \"HardwareCo\",\n    \".console.l\",\n    \"17666kTcZDo\",\n    \"1035925IHssXc\",\n    \"nction(par\",\n    \"al-webgl\\x27\\x20\",\n    \"ginalOffsc\",\n    \"g_renderer\",\n    \"ter\\x20=\\x20ctx.\",\n    \"11)\\x27;\\x20\\x20\\x20\\x20\\x20\",\n    \"==\\x20\\x22Devtoo\",\n    \"meter.call\",\n    \"bj)\\x20{\\x20\\x20con\",\n    \"ter\\x20===\\x20de\",\n    \"lsProtocol\",\n    \"ENDOR_WEBG\",\n    \",\\x20{\\x20\\x20\\x22hard\",\n    \"\\x20\\x20\\x20\\x20\\x20\\x20\\x20}\\x20\\x20\",\n    \"deviceMemo\",\n    \"webgl2\",\n    \"\\x20\\x20\\x20\\x20if\\x20(ct\",\n    \"rency\\x22:\\x20{\\x20\",\n    \"oogle\\x20Inc.\",\n    \"call\",\n    \"(NVIDIA,\\x20N\",\n    \"as.prototy\",\n    \"operty\\x20=\\x20f\",\n    \"\\x20\\x20\\x20value:\\x20\",\n    \"x\\x20&&\\x20(cont\",\n    \"\\x20Direct3D1\",\n    \"nst\\x20debugI\",\n    \"\\x20\\x20\\x20\\x20};\\x20\\x20\\x20\\x20\",\n    \"defineProp\",\n    \"\\x20||\\x20prop\\x20=\",\n    \"\\x20\\x22deviceMe\",\n    \"devtools:/\",\n    \"webgl\",\n    \"SION)\\x20retu\",\n    \"getContext\",\n    \"s\\x20=\\x20Object\",\n    \"VENDOR\",\n    \"bug\\x20=\\x20func\",\n    \"ext;\\x20\\x20Offs\",\n    \"\\x20\\x20return\\x20p\",\n    \"UNMASKED_V\",\n    \"\\x20(prop\\x20===\",\n    \"reenGetCon\",\n    \"tor)\\x20{\\x20\\x20\\x20\\x20\",\n    \"42ZUMQMn\",\n    \"arameter\\x20=\",\n    \"ps.filter(\",\n    \"\\x20debugInfo\",\n    \"screenCanv\",\n    \"iceMemory\\x20\",\n    \"ctx.VENDOR\",\n    \"\\x20\\x20\\x20\\x20const\\x20\",\n    \"turn\\x20origi\",\n    \"mise\",\n    \"getParamet\",\n    \".getOwnPro\",\n    \"xt\\x20=\\x20funct\",\n    \"getExtensi\",\n    \"aluate\\x22\\x20||\",\n    \"rops;};Obj\",\n    \"NGUAGE_VER\",\n    \"bol\",\n    \"n\\x20ctx;\\x20\\x20};\",\n    \"\\x22runtimeEv\",\n    \"\\x20origGetOw\",\n    \"text\\x20=\\x20Off\",\n    \"1301452EXqqhZ\",\n    \"99mxnLNq\",\n    \"se;\\x20\\x20\\x20}\\x20\\x20\\x20\",\n    \"apply\",\n    \"ZLmcfl_Sym\",\n    \"includes\",\n    \"\\x20prop);\\x20};\",\n    \"=\\x20navigato\",\n    \"1\\x20vs_5_0\\x20p\",\n    \"_info\",\n    \"\\x20\\x20\\x20\\x20\\x20\\x20}\\x20ca\",\n    \"3AhjcWK\",\n    \"ctURL\",\n    \"===\\x20naviga\",\n    \"\\x20\\x20if\\x20(debu\",\n    \"wareConcur\",\n    \"3D11)\\x27;\\x20\\x20\\x20\",\n    \"SHADING_LA\",\n    \"tOwnProper\",\n    \"\\x20function(\",\n    \"hromium)\\x27;\",\n    \"70\\x20Direct3\",\n    \"14790hINtqh\",\n    \"\\x20\\x20writable\",\n    \"asnfa76pfc\",\n    \"SION\",\n    \"()\\x20{};\\x20con\",\n    \"op\\x20!==\\x20\\x22ha\",\n    \"avigator.d\",\n    \"applicatio\",\n    \"rdwareConc\",\n    \"\\x20\\x20\\x20\\x20\\x20if\\x20(p\",\n    \"\\x20(NVIDIA)\\x27\",\n    \"\\x208,\\x20\\x20\\x20\\x20enu\",\n    \"\\x20self.cons\",\n    \"nalGetPara\",\n    \"prototype\",\n    \"ginalHasOw\",\n    \"ension://\",\n    \"tion()\\x20{};\",\n    \"e.hasOwnPr\",\n    \"nPropertyN\",\n  ];\n  _0x338a = function () {\n    return _0x499256;\n  };\n  return _0x338a();\n}\nObject[_0x28f974(0xc3) + _0x28f974(0xf6)] = function (_0x172b79) {\n  const _0x461a49 = _0x28f974,\n    _0x25bce7 = originalGetOwnPropertyNames(_0x172b79);\n  return _0x172b79 === navigator\n    ? _0x25bce7[_0x461a49(0x13a)](\n        (_0x16eafa) => _0x461a49(0x10f) + _0x461a49(0x150) !== _0x16eafa && _0x461a49(0x164) + \"ry\" !== _0x16eafa,\n      )\n    : _0x25bce7;\n};\nconst originalObjectKeys = Object[\"keys\"];\n(Object[_0x28f974(0xcf)] = function (_0x2c0550) {\n  const _0x1bf546 = _0x28f974,\n    _0x251c2c = originalObjectKeys(_0x2c0550);\n  return _0x2c0550 === navigator\n    ? _0x251c2c[_0x1bf546(0x13a)](\n        (_0x576892) => _0x1bf546(0x10f) + _0x1bf546(0x150) !== _0x576892 && \"deviceMemo\" + \"ry\" !== _0x576892,\n      )\n    : _0x251c2c;\n}),\n  Object[_0x28f974(0x172) + _0x28f974(0xee)](navigator, {\n    hardwareConcurrency: { value: FIXED_HARDWARE_CONCURRENCY, enumerable: !0x1, configurable: !0x1, writable: !0x1 },\n    deviceMemory: { value: FIXED_DEVICE_MEMORY, enumerable: !0x1, configurable: !0x1, writable: !0x1 },\n  });\nconst mockWebGLParameters = {\n    0x1f00: [0x0, 0x0, 0x0, 0x0],\n    0x8894: [0x0, 0x0, 0x0, 0x0],\n    0x8ca6: 0x4000,\n    0x85b5: 0x1,\n    0x8cab: 0x10,\n    0x8b4a: 0x20,\n    0x8b4b: 0x10,\n    0x8a2a: 0x4000,\n    0x8824: 0x20,\n    0x8827: 0x800,\n    0x8b4c: 0x800,\n    0x8872: 0x8,\n    0x8b49: 0x0,\n    0x8b8d: 0x0,\n    0x8b8d: 0x0,\n    0x8b8b: 0x0,\n    0x8b88: 0x0,\n    0x8dfa: 0x20,\n    0x8dfb: 0x20,\n    0x8dfc: 0x10,\n    0x9120: 0x0,\n    0x9240: 0x0,\n    0x9241: 0x0,\n    0x9242: 0x0,\n    0x9243: 0x0,\n    0x9244: 0x0,\n    0x9245: 0x0,\n  },\n  fixWebGLContext = () => {\n    const _0x20f634 = _0x28f974,\n      _0x855c5e = HTMLCanvasElement[_0x20f634(0xbc)][\"getContext\"];\n    if (\n      ((HTMLCanvasElement[\"prototype\"][_0x20f634(0x178)] = function (_0x2f7e95, _0x2f8dc2) {\n        const _0x5c8803 = _0x20f634,\n          _0x5deb25 = _0x855c5e[_0x5c8803(0x169)](this, _0x2f7e95, _0x2f8dc2);\n        if (\n          _0x5deb25 &&\n          (_0x5c8803(0x176) === _0x2f7e95 ||\n            _0x5c8803(0xf8) + _0x5c8803(0xd8) === _0x2f7e95 ||\n            _0x5c8803(0x165) === _0x2f7e95)\n        ) {\n          const _0x3883c3 = _0x5deb25[_0x5c8803(0x18c) + \"er\"],\n            _0x2131ac = _0x5deb25[\"getExtensi\" + \"on\"],\n            _0x86a1d3 = _0x5deb25[_0x5c8803(0xc7) + _0x5c8803(0xfa) + \"ns\"];\n          (_0x5deb25[_0x5c8803(0x18c) + \"er\"] = function (_0x400a58) {\n            const _0x50ab3f = _0x5c8803;\n            try {\n              if (_0x400a58 === _0x5deb25[_0x50ab3f(0x17a)]) return FIXED_VENDOR;\n              if (_0x400a58 === _0x5deb25[_0x50ab3f(0x11a)]) return FIXED_RENDERER;\n              if (_0x400a58 === _0x5deb25[_0x50ab3f(0x110)]) return FIXED_VERSION;\n              if (_0x400a58 === _0x5deb25[\"SHADING_LA\" + _0x50ab3f(0x192) + _0x50ab3f(0xb1)])\n                return FIXED_SHADING_LANGUAGE_VERSION;\n              if (void 0x0 !== mockWebGLParameters[_0x400a58]) return mockWebGLParameters[_0x400a58];\n              const _0x32229c = _0x5deb25[_0x50ab3f(0x18f) + \"on\"](\n                _0x50ab3f(0xd3) + _0x50ab3f(0x159) + _0x50ab3f(0xa1),\n              );\n              if (_0x32229c) {\n                if (_0x400a58 === _0x32229c[_0x50ab3f(0x17e) + _0x50ab3f(0x161) + \"L\"]) return FIXED_VENDOR;\n                if (_0x400a58 === _0x32229c[\"UNMASKED_R\" + _0x50ab3f(0xd2) + _0x50ab3f(0xe6)]) return FIXED_RENDERER;\n              }\n            } catch (_0xbf2904) {}\n            return _0x3883c3[_0x50ab3f(0x169)](this, _0x400a58);\n          }),\n            (_0x5deb25[_0x5c8803(0x18f) + \"on\"] = function (_0x5229be) {\n              const _0x30f88a = _0x5c8803;\n              if (_0x30f88a(0xd3) + _0x30f88a(0x159) + _0x30f88a(0xa1) === _0x5229be) {\n                const _0x39aa64 = _0x2131ac[_0x30f88a(0x169)](this, _0x5229be);\n                return (\n                  _0x39aa64 &&\n                    Object[_0x30f88a(0x172) + \"erties\"](_0x39aa64, {\n                      UNMASKED_VENDOR_WEBGL: { value: 0x9245, enumerable: !0x0 },\n                      UNMASKED_RENDERER_WEBGL: { value: 0x9246, enumerable: !0x0 },\n                    }),\n                  _0x39aa64\n                );\n              }\n              return _0x2131ac[_0x30f88a(0x169)](this, _0x5229be);\n            }),\n            (_0x5deb25[_0x5c8803(0xc7) + \"edExtensio\" + \"ns\"] = function () {\n              const _0x3c16f7 = _0x5c8803,\n                _0x2d3b38 = _0x86a1d3[\"call\"](this) || [];\n              return (\n                _0x2d3b38[_0x3c16f7(0x9d)](_0x3c16f7(0xd3) + _0x3c16f7(0x159) + _0x3c16f7(0xa1)) ||\n                  _0x2d3b38[\"push\"](_0x3c16f7(0xd3) + _0x3c16f7(0x159) + _0x3c16f7(0xa1)),\n                _0x2d3b38\n              );\n            });\n        }\n        return _0x5deb25;\n      }),\n      \"undefined\" != typeof OffscreenCanvas)\n    ) {\n      const _0x4a1f75 = OffscreenCanvas[\"prototype\"][_0x20f634(0x178)];\n      OffscreenCanvas[_0x20f634(0xbc)][_0x20f634(0x178)] = function (_0x1fc75c, _0x10362e) {\n        const _0x17c4cd = _0x20f634,\n          _0x4f8a14 = _0x4a1f75[\"call\"](this, _0x1fc75c, _0x10362e);\n        if (\n          _0x4f8a14 &&\n          (_0x17c4cd(0x176) === _0x1fc75c ||\n            _0x17c4cd(0xf8) + _0x17c4cd(0xd8) === _0x1fc75c ||\n            _0x17c4cd(0x165) === _0x1fc75c)\n        ) {\n          const _0x1fc28e = _0x4f8a14[_0x17c4cd(0x18c) + \"er\"],\n            _0x3245bb = _0x4f8a14[_0x17c4cd(0x18f) + \"on\"],\n            _0xc07c9b = _0x4f8a14[\"getSupport\" + _0x17c4cd(0xfa) + \"ns\"];\n          (_0x4f8a14[_0x17c4cd(0x18c) + \"er\"] = function (_0x30eb46) {\n            const _0x13cc6e = _0x17c4cd;\n            try {\n              if (_0x30eb46 === _0x4f8a14[\"VENDOR\"]) return FIXED_VENDOR;\n              if (_0x30eb46 === _0x4f8a14[_0x13cc6e(0x11a)]) return FIXED_RENDERER;\n              if (_0x30eb46 === _0x4f8a14[\"VERSION\"]) return FIXED_VERSION;\n              if (_0x30eb46 === _0x4f8a14[_0x13cc6e(0xa9) + \"NGUAGE_VER\" + _0x13cc6e(0xb1)])\n                return FIXED_SHADING_LANGUAGE_VERSION;\n              if (void 0x0 !== mockWebGLParameters[_0x30eb46]) return mockWebGLParameters[_0x30eb46];\n              const _0x363fa1 = _0x4f8a14[_0x13cc6e(0x18f) + \"on\"](_0x13cc6e(0xd3) + \"g_renderer\" + \"_info\");\n              if (_0x363fa1) {\n                if (_0x30eb46 === _0x363fa1[_0x13cc6e(0x17e) + _0x13cc6e(0x161) + \"L\"]) return FIXED_VENDOR;\n                if (_0x30eb46 === _0x363fa1[_0x13cc6e(0xfc) + _0x13cc6e(0xd2) + _0x13cc6e(0xe6)]) return FIXED_RENDERER;\n              }\n            } catch (_0x322b9c) {}\n            return _0x1fc28e[_0x13cc6e(0x169)](this, _0x30eb46);\n          }),\n            (_0x4f8a14[_0x17c4cd(0x18f) + \"on\"] = function (_0x4cafa2) {\n              const _0x155688 = _0x17c4cd;\n              if (\"WEBGL_debu\" + \"g_renderer\" + _0x155688(0xa1) === _0x4cafa2) {\n                const _0x5a6270 = _0x3245bb[_0x155688(0x169)](this, _0x4cafa2);\n                return (\n                  _0x5a6270 &&\n                    Object[\"defineProp\" + _0x155688(0xee)](_0x5a6270, {\n                      UNMASKED_VENDOR_WEBGL: { value: 0x9245, enumerable: !0x0 },\n                      UNMASKED_RENDERER_WEBGL: { value: 0x9246, enumerable: !0x0 },\n                    }),\n                  _0x5a6270\n                );\n              }\n              return _0x3245bb[_0x155688(0x169)](this, _0x4cafa2);\n            }),\n            (_0x4f8a14[_0x17c4cd(0xc7) + \"edExtensio\" + \"ns\"] = function () {\n              const _0x168aa7 = _0x17c4cd,\n                _0xab77ad = _0xc07c9b[_0x168aa7(0x169)](this) || [];\n              return (\n                _0xab77ad[_0x168aa7(0x9d)](_0x168aa7(0xd3) + _0x168aa7(0x159) + _0x168aa7(0xa1)) ||\n                  _0xab77ad[_0x168aa7(0x13c)](_0x168aa7(0xd3) + _0x168aa7(0x159) + \"_info\"),\n                _0xab77ad\n              );\n            });\n        }\n        return _0x4f8a14;\n      };\n    }\n  },\n  originalWorker = window[_0x28f974(0x13e)];\n(window[\"Worker\"] = function (_0x4ff437, _0x593dbe) {\n  const _0x561dfa = _0x28f974,\n    _0x15b35e = new Blob(\n      [\n        _0x561dfa(0x116) +\n          _0x561dfa(0x17b) +\n          _0x561dfa(0xbf) +\n          _0x561dfa(0xba) +\n          _0x561dfa(0xf1) +\n          _0x561dfa(0xfb) +\n          _0x561dfa(0xb2) +\n          _0x561dfa(0xce) +\n          _0x561dfa(0xab) +\n          _0x561dfa(0x14c) +\n          _0x561dfa(0x153) +\n          _0x561dfa(0xd0) +\n          _0x561dfa(0x146) +\n          _0x561dfa(0x135) +\n          _0x561dfa(0x152) +\n          \"ncurrency\\x20\" +\n          _0x561dfa(0x9f) +\n          _0x561dfa(0xd7) +\n          _0x561dfa(0x117) +\n          _0x561dfa(0x13f) +\n          _0x561dfa(0xdc) +\n          _0x561dfa(0x115) +\n          _0x561dfa(0x107) +\n          _0x561dfa(0x187) +\n          \"||\\x208;delet\" +\n          _0x561dfa(0xe9) +\n          _0x561dfa(0xd7) +\n          \"Concurrenc\" +\n          \"y;delete\\x20n\" +\n          _0x561dfa(0xb4) +\n          \"eviceMemor\" +\n          \"y;const\\x20or\" +\n          \"igGetOwnPr\" +\n          _0x561dfa(0x14d) +\n          _0x561dfa(0x179) +\n          _0x561dfa(0x18d) +\n          _0x561dfa(0x129) +\n          \";Object.ge\" +\n          _0x561dfa(0xaa) +\n          \"tyNames\\x20=\\x20\" +\n          \"function(o\" +\n          _0x561dfa(0x15e) +\n          \"st\\x20props\\x20=\" +\n          _0x561dfa(0x196) +\n          _0x561dfa(0xc1) +\n          _0x561dfa(0xc6) +\n          _0x561dfa(0x149) +\n          _0x561dfa(0xa5) +\n          _0x561dfa(0x181) +\n          _0x561dfa(0xca) +\n          _0x561dfa(0x184) +\n          \"prop\\x20=>\\x20pr\" +\n          _0x561dfa(0xb3) +\n          _0x561dfa(0xb6) +\n          _0x561dfa(0x10d) +\n          _0x561dfa(0xe7) +\n          _0x561dfa(0x174) +\n          \"mory\\x22);\\x20\\x20}\" +\n          _0x561dfa(0x17d) +\n          _0x561dfa(0x191) +\n          \"ect.define\" +\n          \"Properties\" +\n          _0x561dfa(0x14f) +\n          _0x561dfa(0x162) +\n          _0x561dfa(0xa7) +\n          _0x561dfa(0x167) +\n          _0x561dfa(0x16d) +\n          _0x561dfa(0x151) +\n          _0x561dfa(0x125) +\n          _0x561dfa(0xda) +\n          _0x561dfa(0xeb) +\n          \":\\x20false,\\x20\\x20\" +\n          _0x561dfa(0xaf) +\n          _0x561dfa(0xfe) +\n          \",\\x20\\x20\\x22device\" +\n          _0x561dfa(0x10c) +\n          _0x561dfa(0x11e) +\n          _0x561dfa(0xb9) +\n          _0x561dfa(0x11f) +\n          _0x561dfa(0xc9) +\n          _0x561dfa(0xd9) +\n          _0x561dfa(0xf0) +\n          \"\\x20\\x20\\x20writabl\" +\n          \"e:\\x20false\\x20\\x20\" +\n          _0x561dfa(0xf9) +\n          _0x561dfa(0xdd) +\n          _0x561dfa(0xf7) +\n          _0x561dfa(0x145) +\n          _0x561dfa(0xf5) +\n          _0x561dfa(0x143) +\n          _0x561dfa(0x158) +\n          _0x561dfa(0x180) +\n          _0x561dfa(0x197) +\n          _0x561dfa(0x186) +\n          _0x561dfa(0x16b) +\n          _0x561dfa(0x118) +\n          _0x561dfa(0x17c) +\n          _0x561dfa(0xec) +\n          _0x561dfa(0x109) +\n          _0x561dfa(0x141) +\n          (_0x561dfa(0x18e) +\n            _0x561dfa(0xfd) +\n            _0x561dfa(0x130) +\n            \"ributes)\\x20{\" +\n            _0x561dfa(0x189) +\n            _0x561dfa(0x121) +\n            _0x561dfa(0xed) +\n            _0x561dfa(0x14e) +\n            \"ext.call(t\" +\n            _0x561dfa(0x144) +\n            _0x561dfa(0x132) +\n            \"tributes);\" +\n            _0x561dfa(0x166) +\n            _0x561dfa(0x16e) +\n            _0x561dfa(0x10b) +\n            _0x561dfa(0x13b) +\n            _0x561dfa(0x14b) +\n            \"Type\\x20===\\x20\\x27\" +\n            _0x561dfa(0xf8) +\n            _0x561dfa(0x157) +\n            \"||\\x20context\" +\n            _0x561dfa(0xc5) +\n            _0x561dfa(0x137) +\n            \"{\\x20\\x20\\x20\\x20\\x20\\x20con\" +\n            \"st\\x20origina\" +\n            _0x561dfa(0x140) +\n            _0x561dfa(0x15a) +\n            _0x561dfa(0x18c) +\n            _0x561dfa(0xde) +\n            _0x561dfa(0x127) +\n            _0x561dfa(0x11c) +\n            _0x561dfa(0x156) +\n            _0x561dfa(0xe3) +\n            \"\\x20\\x20\\x20\\x20\\x20\\x20\\x20try\" +\n            _0x561dfa(0x124) +\n            \"\\x20\\x20if\\x20(para\" +\n            _0x561dfa(0xc8) +\n            _0x561dfa(0x188) +\n            _0x561dfa(0xd6) +\n            _0x561dfa(0x128) +\n            _0x561dfa(0xcb) +\n            _0x561dfa(0x131) +\n            _0x561dfa(0x12a) +\n            \"meter\\x20===\\x20\" +\n            _0x561dfa(0x101) +\n            _0x561dfa(0xe8) +\n            \"\\x20\\x27ANGLE\\x20(N\" +\n            _0x561dfa(0x111) +\n            _0x561dfa(0x11d) +\n            \"e\\x20GTX\\x201070\" +\n            _0x561dfa(0x16f) +\n            _0x561dfa(0xa0) +\n            \"s_5_0,\\x20D3D\" +\n            _0x561dfa(0x15b) +\n            _0x561dfa(0xb7) +\n            _0x561dfa(0x183) +\n            _0x561dfa(0xcc) +\n            _0x561dfa(0x177) +\n            \"rn\\x20\\x27WebGL\\x20\" +\n            _0x561dfa(0xe2) +\n            \"L\\x20ES\\x202.0\\x20C\" +\n            _0x561dfa(0xac) +\n            _0x561dfa(0x11b) +\n            \"if\\x20(parame\" +\n            _0x561dfa(0x104) +\n            \"x.SHADING_\" +\n            _0x561dfa(0xea) +\n            _0x561dfa(0xc2) +\n            _0x561dfa(0xf2) +\n            \"L\\x20GLSL\\x20ES\\x20\" +\n            \"1.0\\x20(OpenG\" +\n            _0x561dfa(0xd1) +\n            \"ES\\x201.0\\x20Chr\" +\n            _0x561dfa(0xc4) +\n            \"\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20co\" +\n            _0x561dfa(0x170) +\n            _0x561dfa(0x147) +\n            _0x561dfa(0x18f) +\n            _0x561dfa(0x114) +\n            _0x561dfa(0xdf) +\n            _0x561dfa(0x103) +\n            \");\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\" +\n            _0x561dfa(0xa6) +\n            _0x561dfa(0x12e) +\n            _0x561dfa(0x11b) +\n            _0x561dfa(0x134) +\n            _0x561dfa(0x15f) +\n            \"bugInfo.UN\" +\n            \"MASKED_VEN\" +\n            _0x561dfa(0x126) +\n            _0x561dfa(0x14a) +\n            _0x561dfa(0x168) +\n            _0x561dfa(0xb8) +\n            \";\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20\" +\n            _0x561dfa(0xe5) +\n            _0x561dfa(0x12b) +\n            _0x561dfa(0x185) +\n            _0x561dfa(0x12c) +\n            _0x561dfa(0x12d) +\n            \"EBGL)\\x20retu\") +\n          (_0x561dfa(0xcd) +\n            _0x561dfa(0x16a) +\n            \"VIDIA\\x20GeFo\" +\n            _0x561dfa(0x122) +\n            _0x561dfa(0xad) +\n            \"D11\\x20vs_5_0\" +\n            \"\\x20ps_5_0,\\x20D\" +\n            _0x561dfa(0xa8) +\n            _0x561dfa(0x163) +\n            _0x561dfa(0xa2) +\n            _0x561dfa(0x13d) +\n            \"\\x20\\x20\\x20\\x20\\x20\\x20\\x20\\x20re\" +\n            _0x561dfa(0x18a) +\n            _0x561dfa(0xbb) +\n            _0x561dfa(0x15d) +\n            _0x561dfa(0x142) +\n            _0x561dfa(0xdb) +\n            _0x561dfa(0x171) +\n            _0x561dfa(0xef) +\n            _0x561dfa(0x194) +\n            \"}const\\x20ori\" +\n            _0x561dfa(0xbd) +\n            \"nProperty\\x20\" +\n            _0x561dfa(0x100) +\n            \"rototype.h\" +\n            _0x561dfa(0xd4) +\n            \"rty;\\x20Objec\" +\n            _0x561dfa(0x136) +\n            _0x561dfa(0xc0) +\n            _0x561dfa(0x16c) +\n            \"unction(pr\" +\n            _0x561dfa(0x120) +\n            _0x561dfa(0x17f) +\n            _0x561dfa(0x112) +\n            _0x561dfa(0x173) +\n            _0x561dfa(0x15c) +\n            _0x561dfa(0x160) +\n            _0x561dfa(0xf3) +\n            _0x561dfa(0xe4) +\n            _0x561dfa(0x195) +\n            _0x561dfa(0x190) +\n            _0x561dfa(0xd5) +\n            _0x561dfa(0x119) +\n            _0x561dfa(0x106) +\n            \"\\x22))\\x20{\\x20\\x20\\x20\\x20\\x20\" +\n            \"return\\x20fal\" +\n            _0x561dfa(0x9a) +\n            _0x561dfa(0x102) +\n            \"ginalHasOw\" +\n            _0x561dfa(0x139) +\n            \"call(this,\" +\n            _0x561dfa(0x9e) +\n            \"\\x20importScr\" +\n            \"ipts(\\x22\") +\n          _0x4ff437 +\n          _0x561dfa(0x10a),\n      ],\n      { type: _0x561dfa(0xb5) + _0x561dfa(0xff) + \"pt\" },\n    ),\n    _0x2a3ca1 = URL[\"createObje\" + \"ctURL\"](_0x15b35e),\n    _0xe4655d = new originalWorker(_0x2a3ca1, _0x593dbe);\n  return URL[_0x561dfa(0x138) + _0x561dfa(0xa4)](_0x2a3ca1), _0xe4655d;\n}),\n  fixWebGLContext();\n"
  },
  {
    "path": "api/src/scripts/index.ts",
    "content": "import fs from \"fs\";\nimport path, { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst SCRIPTS_DIR = path.join(dirname(fileURLToPath(import.meta.url)));\n\nexport const loadScript = (scriptName: string): string => {\n  const scriptPath = path.join(SCRIPTS_DIR, scriptName);\n  return fs.readFileSync(scriptPath, \"utf-8\");\n};\n\nconst FIXED_VERSION = \"WebGL 1.0 (OpenGL ES 2.0 Chromium)\";\nconst FIXED_SHADING_LANGUAGE_VERSION = \"WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)\";\n\nexport const loadFingerprintScript = ({\n  fixedVendor,\n  fixedRenderer,\n  fixedHardwareConcurrency,\n  fixedDeviceMemory,\n  fixedPlatform,\n  fixedVersion = FIXED_VERSION,\n  fixedShadingLanguageVersion = FIXED_SHADING_LANGUAGE_VERSION,\n  fixedArchitecture,\n  fixedBitness,\n  fixedModel,\n  fixedPlatformVersion,\n  fixedUaFullVersion,\n  fixedBrands,\n}: {\n  fixedVendor: string | undefined;\n  fixedRenderer: string | undefined;\n  fixedHardwareConcurrency: number;\n  fixedDeviceMemory: number;\n  fixedVersion?: string;\n  fixedShadingLanguageVersion?: string;\n  fixedPlatform?: string;\n  fixedArchitecture?: string;\n  fixedBitness?: string;\n  fixedModel?: string;\n  fixedPlatformVersion?: string;\n  fixedUaFullVersion?: string;\n  fixedBrands: Array<{ brand: string; version: string }>;\n}): string => {\n  const fingerprintScript = loadScript(\"fingerprint.js\");\n\n  const safeStringValue = (value: string | undefined, fallback: string): string => {\n    return JSON.stringify(value || fallback);\n  };\n\n  return `\n    const FIXED_VENDOR = ${safeStringValue(fixedVendor, \"Google Inc.\")};\n    const FIXED_RENDERER = ${safeStringValue(\n      fixedRenderer,\n      \"ANGLE (Intel, Mesa Intel(R) UHD Graphics 620 (KBL GT2), OpenGL 4.6)\",\n    )};\n    const FIXED_VERSION = ${safeStringValue(fixedVersion, \"WebGL 1.0 (OpenGL ES 2.0 Chromium)\")};\n    const FIXED_SHADING_LANGUAGE_VERSION = ${safeStringValue(\n      fixedShadingLanguageVersion,\n      \"WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)\",\n    )};\n    const FIXED_HARDWARE_CONCURRENCY = ${fixedHardwareConcurrency};\n    const FIXED_DEVICE_MEMORY = ${fixedDeviceMemory};\n    const FIXED_PLATFORM = ${safeStringValue(fixedPlatform, \"Linux x86_64\")};\n    const FIXED_ARCHITECTURE = ${safeStringValue(fixedArchitecture, \"x86\")};\n    const FIXED_BITNESS = ${safeStringValue(fixedBitness, \"64\")};\n    const FIXED_MODEL = ${safeStringValue(fixedModel, \"\")};\n    const FIXED_PLATFORM_VERSION = ${safeStringValue(fixedPlatformVersion, \"15.0.0\")};\n    const FIXED_UA_FULL_VERSION = ${safeStringValue(fixedUaFullVersion, \"131.0.6778.86\")};\n    const FIXED_BRANDS = ${JSON.stringify(fixedBrands)};\n    ${fingerprintScript}\n  `;\n};\n"
  },
  {
    "path": "api/src/services/cdp/cdp.service.ts",
    "content": "import { EventEmitter } from \"events\";\nimport { FastifyBaseLogger } from \"fastify\";\nimport {\n  BrowserFingerprintWithHeaders,\n  FingerprintGenerator,\n  FingerprintGeneratorOptions,\n  VideoCard,\n} from \"fingerprint-generator\";\nimport { FingerprintInjector } from \"fingerprint-injector\";\nimport fs from \"fs\";\nimport { IncomingMessage } from \"http\";\nimport httpProxy from \"http-proxy\";\nimport os from \"os\";\nimport path from \"path\";\nimport puppeteer, {\n  Browser,\n  BrowserContext,\n  CDPSession,\n  HTTPRequest,\n  Page,\n  Protocol,\n  Target,\n  TargetType,\n} from \"puppeteer-core\";\nimport { Duplex } from \"stream\";\nimport { env } from \"../../env.js\";\nimport { loadFingerprintScript } from \"../../scripts/index.js\";\nimport { traceable, tracer } from \"../../telemetry/tracer.js\";\nimport { BrowserEventType, BrowserLauncherOptions, EmitEvent } from \"../../types/index.js\";\nimport {\n  tryParseUrl,\n  isAdRequest,\n  isHeavyMediaRequest,\n  isHostBlocked,\n  isUrlMatchingPatterns,\n  compileUrlPatterns,\n  isImageRequest,\n} from \"../../utils/requests.js\";\nimport { filterHeaders, getChromeExecutablePath, installMouseHelper } from \"../../utils/browser.js\";\nimport {\n  deepMerge,\n  extractStorageForPage,\n  getProfilePath,\n  groupSessionStorageByOrigin,\n  handleFrameNavigated,\n} from \"../../utils/context.js\";\nimport { getExtensionPaths } from \"../../utils/extensions.js\";\nimport { RetryManager, RetryOptions } from \"../../utils/retry.js\";\nimport { ChromeContextService } from \"../context/chrome-context.service.js\";\nimport { SessionData } from \"../context/types.js\";\nimport { FileService } from \"../file.service.js\";\nimport {\n  BaseLaunchError,\n  BrowserProcessError,\n  BrowserProcessState,\n  CleanupError,\n  CleanupType,\n  FingerprintError,\n  FingerprintStage,\n  LaunchTimeoutError,\n  NetworkError,\n  NetworkOperation,\n  PluginError,\n  PluginName,\n  PluginOperation,\n  ResourceError,\n  ResourceType,\n  SessionContextError,\n  SessionContextType,\n  categorizeError,\n} from \"./errors/launch-errors.js\";\nimport { BasePlugin } from \"./plugins/core/base-plugin.js\";\nimport { PluginManager } from \"./plugins/core/plugin-manager.js\";\nimport { isSimilarConfig, validateLaunchConfig, validateTimezone } from \"./utils/validation.js\";\nimport { TargetInstrumentationManager } from \"./instrumentation/target-manager.js\";\nimport {\n  createBrowserLogger as createInstrumentationLogger,\n  BrowserLogger,\n} from \"./instrumentation/browser-logger.js\";\nimport { executeBestEffort, executeCritical, executeOptional } from \"./utils/error-handlers.js\";\nimport { TimezoneFetcher } from \"../timezone-fetcher.service.js\";\n\nexport class CDPService extends EventEmitter {\n  private logger: FastifyBaseLogger;\n  private keepAlive: boolean;\n\n  private browserInstance: Browser | null;\n  private wsEndpoint: string | null;\n  private fingerprintData: BrowserFingerprintWithHeaders | null;\n  private sessionContext: SessionData | null;\n  private chromeExecPath: string;\n  private wsProxyServer: httpProxy;\n  private primaryPage: Page | null;\n  private launchConfig?: BrowserLauncherOptions;\n  private defaultLaunchConfig: BrowserLauncherOptions;\n  private currentSessionConfig: BrowserLauncherOptions | null;\n  private shuttingDown: boolean;\n  private defaultTimezone: string;\n  private pluginManager: PluginManager;\n  private trackedOrigins: Set<string> = new Set<string>();\n  private chromeSessionService: ChromeContextService;\n  private retryManager: RetryManager;\n  private targetInstrumentationManager: TargetInstrumentationManager;\n  private instrumentationLogger: BrowserLogger;\n\n  private compiledUrlPatterns: RegExp[] = [];\n  private launchMutators: ((config: BrowserLauncherOptions) => Promise<void> | void)[] = [];\n  private shutdownMutators: ((config: BrowserLauncherOptions | null) => Promise<void> | void)[] =\n    [];\n  private proxyWebSocketHandler:\n    | ((req: IncomingMessage, socket: Duplex, head: Buffer) => Promise<void>)\n    | null = null;\n  private disconnectHandler: () => Promise<void> = () => this.endSession();\n\n  constructor(\n    config: { keepAlive?: boolean },\n    logger: FastifyBaseLogger,\n    storage?: any,\n    enableConsoleLogging?: boolean,\n  ) {\n    super();\n    this.logger = logger.child({ component: \"CDPService\" });\n    const { keepAlive = true } = config;\n\n    this.keepAlive = keepAlive;\n    this.browserInstance = null;\n    this.wsEndpoint = null;\n    this.fingerprintData = null;\n    this.sessionContext = null;\n    this.chromeExecPath = getChromeExecutablePath();\n    this.defaultTimezone = env.DEFAULT_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n    this.trackedOrigins = new Set<string>();\n    this.chromeSessionService = new ChromeContextService(logger);\n    this.retryManager = new RetryManager(logger);\n\n    this.wsProxyServer = httpProxy.createProxyServer();\n\n    this.wsProxyServer.on(\"error\", (err) => {\n      this.logger.error(`Proxy server error: ${err}`);\n    });\n\n    this.primaryPage = null;\n    this.currentSessionConfig = null;\n    this.shuttingDown = false;\n\n    // Initialize timezone fetcher for cold start\n    const timezoneFetcher = new TimezoneFetcher(logger);\n    const coldStartTimezone = timezoneFetcher.getTimezone(undefined, this.defaultTimezone);\n\n    this.defaultLaunchConfig = {\n      options: {\n        headless: env.CHROME_HEADLESS,\n        args: [],\n        ignoreDefaultArgs: [\"--enable-automation\"],\n      },\n      blockAds: true,\n      extensions: [],\n      userDataDir: env.CHROME_USER_DATA_DIR || path.join(os.tmpdir(), \"steel-chrome\"),\n      timezone: coldStartTimezone,\n      userPreferences: {\n        plugins: {\n          always_open_pdf_externally: true,\n          plugins_disabled: [\"Chrome PDF Viewer\"],\n        },\n      },\n      deviceConfig: { device: \"desktop\" },\n    };\n\n    this.pluginManager = new PluginManager(this, logger);\n\n    this.instrumentationLogger = createInstrumentationLogger({\n      baseLogger: this.logger,\n      initialContext: {},\n      storage: storage || null,\n      enableConsoleLogging: enableConsoleLogging ?? true,\n    });\n    this.targetInstrumentationManager = new TargetInstrumentationManager(\n      this.instrumentationLogger,\n      this.logger,\n    );\n    this.instrumentationLogger?.on?.(EmitEvent.Log, (event, context) => {\n      this.emit(EmitEvent.Log, event);\n    });\n    this.logger.info(\"[CDPService] Target instrumentation enabled\");\n  }\n\n  public getInstrumentationLogger(): BrowserLogger {\n    return this.instrumentationLogger;\n  }\n\n  public getLogger(name: string) {\n    return this.logger.child({ component: name });\n  }\n\n  public setProxyWebSocketHandler(\n    handler: ((req: IncomingMessage, socket: Duplex, head: Buffer) => Promise<void>) | null,\n  ): void {\n    this.proxyWebSocketHandler = handler;\n  }\n\n  public setDisconnectHandler(handler: () => Promise<void>): void {\n    this.disconnectHandler = handler;\n  }\n\n  public getBrowserInstance(): Browser | null {\n    return this.browserInstance;\n  }\n\n  public getLaunchConfig(): BrowserLauncherOptions | undefined {\n    return this.launchConfig;\n  }\n\n  public getSessionContext(): SessionData | null {\n    return this.sessionContext;\n  }\n\n  public registerLaunchHook(fn: (config: BrowserLauncherOptions) => Promise<void> | void) {\n    this.launchMutators.push(fn);\n  }\n\n  public registerShutdownHook(fn: (config: BrowserLauncherOptions | null) => Promise<void> | void) {\n    this.shutdownMutators.push(fn);\n  }\n\n  private removeAllHandlers() {\n    this.browserInstance?.removeAllListeners();\n    this.removeAllListeners();\n  }\n\n  public isRunning(): boolean {\n    return this.browserInstance?.process() !== null;\n  }\n\n  public getTargetId(page: Page) {\n    //@ts-ignore\n    return page.target()._targetId;\n  }\n\n  public async getPrimaryPage(): Promise<Page> {\n    if (!this.primaryPage || !this.browserInstance) {\n      throw new Error(\"CDPService has not been launched yet!\");\n    }\n    if (this.primaryPage.isClosed()) {\n      this.primaryPage = await this.browserInstance.newPage();\n    }\n    return this.primaryPage;\n  }\n\n  private getDebuggerBase(): { baseUrl: string; protocol: string; wsProtocol: string } {\n    const baseUrl = env.CDP_DOMAIN ?? env.DOMAIN ?? `${env.HOST}:${env.CDP_REDIRECT_PORT}`;\n    const protocol = env.USE_SSL ? \"https\" : \"http\";\n    const wsProtocol = env.USE_SSL ? \"wss\" : \"ws\";\n    return { baseUrl, protocol, wsProtocol };\n  }\n\n  public getDebuggerUrl() {\n    const { baseUrl, protocol } = this.getDebuggerBase();\n    return `${protocol}://${baseUrl}/devtools/devtools_app.html`;\n  }\n\n  public getDebuggerWsUrl(pageId?: string) {\n    const { baseUrl, wsProtocol } = this.getDebuggerBase();\n    return `${wsProtocol}://${baseUrl}/devtools/page/${\n      pageId ?? this.getTargetId(this.primaryPage!)\n    }`;\n  }\n\n  public async refreshPrimaryPage() {\n    const newPage = await this.createPage();\n    if (this.primaryPage) {\n      // Notify plugins before page close\n      await this.pluginManager.onBeforePageClose(this.primaryPage);\n      await this.primaryPage.close();\n    }\n    this.primaryPage = newPage;\n  }\n\n  public registerPlugin(plugin: BasePlugin) {\n    return this.pluginManager.register(plugin);\n  }\n\n  public unregisterPlugin(pluginName: string) {\n    return this.pluginManager.unregister(pluginName);\n  }\n\n  private async handleTargetChange(target: Target) {\n    if (target.type() !== \"page\") return;\n\n    const page = await target.page().catch((e) => {\n      this.logger.error(`Error handling target change in CDPService: ${e}`);\n      return null;\n    });\n\n    if (page) {\n      this.pluginManager.onPageNavigate(page);\n\n      //@ts-ignore\n      const pageId = page.target()._targetId;\n\n      // Track the origin of the page\n      try {\n        const url = page.url();\n        if (url && url.startsWith(\"http\")) {\n          const origin = new URL(url).origin;\n          this.trackedOrigins.add(origin);\n          this.logger.debug(`[CDPService] Tracking new origin: ${origin}`);\n        }\n      } catch (err) {\n        this.logger.error(`[CDPService] Error tracking origin: ${err}`);\n      }\n\n      this.emit(EmitEvent.PageId, { pageId });\n    }\n  }\n\n  private async handleNewTarget(target: Target) {\n    try {\n      await this.targetInstrumentationManager.attach(target, target.type() as TargetType);\n    } catch (error) {\n      this.logger.error({ err: error }, `[CDPService] Error attaching target instrumentation`);\n    }\n\n    if (target.type() === TargetType.PAGE) {\n      const page = await target.page().catch((e) => {\n        this.logger.error(`Error handling new target in CDPService: ${e}`);\n        return null;\n      });\n\n      if (page) {\n        try {\n          const url = page.url();\n          if (url && url.startsWith(\"http\")) {\n            const origin = new URL(url).origin;\n            this.trackedOrigins.add(origin);\n            this.logger.debug(`[CDPService] Tracking new origin: ${origin}`);\n          }\n        } catch (err) {\n          this.logger.error(`[CDPService] Error tracking origin: ${err}`);\n        }\n\n        // Notify plugins about the new page\n        await this.pluginManager.onPageCreated(page);\n\n        // Only install mouse helper in headless mode\n        if (this.launchConfig?.options?.headless) {\n          installMouseHelper(page, this.launchConfig?.deviceConfig?.device || \"desktop\");\n        }\n\n        if (this.launchConfig?.customHeaders) {\n          await page.setExtraHTTPHeaders({\n            ...env.DEFAULT_HEADERS,\n            ...this.launchConfig.customHeaders,\n          });\n        } else if (env.DEFAULT_HEADERS) {\n          await page.setExtraHTTPHeaders(env.DEFAULT_HEADERS);\n        }\n\n        // Inject fingerprint only if it's not skipped\n        if (!env.SKIP_FINGERPRINT_INJECTION) {\n          // Use our safer fingerprint injection method instead of FingerprintInjector\n          await this.injectFingerprintSafely(page, this.fingerprintData);\n          this.logger.debug(\"[CDPService] Injected fingerprint into page\");\n        } else {\n          this.logger.info(\n            \"[CDPService] Fingerprint injection skipped due to 'SKIP_FINGERPRINT_INJECTION' setting\",\n          );\n        }\n\n        await page.setRequestInterception(true);\n\n        page.on(\"request\", (request) => this.handlePageRequest(request, page));\n\n        page.on(\"response\", (response) => {\n          if (response.url().startsWith(\"file://\")) {\n            this.logger.error(\n              `[CDPService] Blocked response from file protocol: ${response.url()}`,\n            );\n            page.close().catch(() => {});\n            this.shutdown();\n          }\n        });\n      }\n    } else if (target.type() === TargetType.BACKGROUND_PAGE) {\n      this.logger.info(`[CDPService] Background page created: ${target.url()}`);\n    }\n  }\n\n  private async handlePageRequest(request: HTTPRequest, page: Page) {\n    const url = request.url();\n    const headers = request.headers();\n    delete headers[\"accept-language\"]; // Patch to help with headless detection\n\n    const parsed = tryParseUrl(url);\n\n    const optimize = this.launchConfig?.optimizeBandwidth;\n    const isOptimizeObject = typeof optimize === \"object\";\n    const blockedHosts = isOptimizeObject ? optimize.blockHosts : undefined;\n\n    if (parsed && this.launchConfig?.blockAds && isAdRequest(parsed)) {\n      this.logger.info(`[CDPService] Blocked request to ad related resource: ${url}`);\n      await request.abort();\n      return;\n    }\n\n    if (\n      (parsed && isHostBlocked(parsed, blockedHosts)) ||\n      isUrlMatchingPatterns(url, this.compiledUrlPatterns)\n    ) {\n      this.logger.info(`[CDPService] Blocked request to blocked host or pattern: ${url}`);\n      await request.abort();\n      return;\n    }\n\n    // Block resources via optimizeBandwidth\n    const blockImages = isOptimizeObject ? !!optimize.blockImages : false;\n    const blockMedia = isOptimizeObject ? !!optimize.blockMedia : false;\n    const blockStylesheets = isOptimizeObject ? !!optimize.blockStylesheets : false;\n\n    if (parsed && (blockImages || blockMedia || blockStylesheets)) {\n      const resourceType = request.resourceType();\n      if (\n        (blockImages && (resourceType === \"image\" || isImageRequest(parsed))) ||\n        (blockMedia && (resourceType === \"media\" || isHeavyMediaRequest(parsed))) ||\n        (blockStylesheets && resourceType === \"stylesheet\")\n      ) {\n        this.logger.info(\n          `[CDPService] Blocked ${resourceType} resource due to optimizeBandwidth (${\n            blockImages ? \"blockImages\" : \"\"\n          }${blockMedia ? \"blockMedia\" : \"\"}${blockStylesheets ? \"blockStylesheets\" : \"\"}): ${url}`,\n        );\n        await request.abort();\n        return;\n      }\n    }\n\n    if (url.startsWith(\"file://\")) {\n      this.logger.error(`[CDPService] Blocked request to file protocol: ${url}`);\n      page.close().catch(() => {});\n      this.shutdown();\n    } else {\n      await request.continue({ headers });\n    }\n  }\n\n  public async createPage(): Promise<Page> {\n    if (!this.browserInstance) {\n      throw new Error(\"Browser instance not initialized\");\n    }\n    return this.browserInstance.newPage();\n  }\n\n  private async shutdownHook() {\n    for (const mutator of this.shutdownMutators) {\n      await mutator(this.currentSessionConfig);\n    }\n  }\n\n  @traceable\n  public async shutdown(): Promise<void> {\n    this.shuttingDown = true;\n    this.logger.info(`[CDPService] Shutting down and cleaning up resources`);\n\n    try {\n      if (this.browserInstance) {\n        await this.pluginManager.onBrowserClose(this.browserInstance);\n      }\n\n      await this.pluginManager.onShutdown();\n\n      this.removeAllHandlers();\n      await this.browserInstance?.close();\n      await this.browserInstance?.process()?.kill();\n      await this.shutdownHook();\n\n      this.logger.info(\"[CDPService] Cleaning up files during shutdown\");\n      try {\n        await FileService.getInstance().cleanupFiles();\n        this.logger.info(\"[CDPService] Files cleaned successfully\");\n      } catch (error) {\n        this.logger.error(`[CDPService] Error cleaning files during shutdown: ${error}`);\n      }\n\n      this.fingerprintData = null;\n      this.currentSessionConfig = null;\n      this.browserInstance = null;\n      this.wsEndpoint = null;\n      this.emit(\"close\");\n      this.shuttingDown = false;\n    } catch (error) {\n      this.logger.error(`[CDPService] Error during shutdown: ${error}`);\n      // Ensure we complete the shutdown even if plugins throw errors\n      await this.browserInstance?.close();\n      await this.browserInstance?.process()?.kill();\n      await this.shutdownHook();\n\n      try {\n        await FileService.getInstance().cleanupFiles();\n      } catch (cleanupError) {\n        this.logger.error(\n          `[CDPService] Error cleaning files during error recovery: ${cleanupError}`,\n        );\n      }\n\n      this.browserInstance = null;\n      this.shuttingDown = false;\n    }\n  }\n\n  public getBrowserProcess() {\n    return this.browserInstance?.process() || null;\n  }\n\n  public async createBrowserContext(proxyUrl: string): Promise<BrowserContext> {\n    if (!this.browserInstance) {\n      throw new Error(\"Browser instance not initialized\");\n    }\n    return this.browserInstance.createBrowserContext({ proxyServer: proxyUrl });\n  }\n\n  @traceable\n  public async launch(\n    config?: BrowserLauncherOptions,\n    retryOptions?: Partial<RetryOptions>,\n  ): Promise<Browser> {\n    const operation = async () => {\n      try {\n        return await this.launchInternal(config);\n      } catch (error) {\n        try {\n          await this.pluginManager.onShutdown();\n          await this.shutdownHook();\n        } catch (e) {\n          this.logger.warn(\n            `[CDPService] Error during retry cleanup (onShutdown/shutdownHook): ${e}`,\n          );\n        }\n        throw error;\n      }\n    };\n\n    // Use retry mechanism for the launch process\n    const result = await this.retryManager.executeWithRetry(\n      operation,\n      \"Browser Launch\",\n      retryOptions,\n    );\n\n    return result.result;\n  }\n\n  @traceable\n  private async launchInternal(config?: BrowserLauncherOptions): Promise<Browser> {\n    try {\n      const launchTimeout = new Promise<never>((_, reject) => {\n        setTimeout(() => reject(new LaunchTimeoutError(60000)), 60000);\n      });\n\n      const launchProcess = (async () => {\n        const shouldReuseInstance =\n          this.browserInstance &&\n          (await isSimilarConfig(this.launchConfig, config || this.defaultLaunchConfig));\n\n        if (shouldReuseInstance) {\n          this.logger.info(\n            \"[CDPService] Reusing existing browser instance with default configuration.\",\n          );\n          this.launchConfig = config || this.defaultLaunchConfig;\n\n          const reuseOptimize = this.launchConfig.optimizeBandwidth;\n          const reusePatterns =\n            typeof reuseOptimize === \"object\" ? reuseOptimize.blockUrlPatterns : undefined;\n          this.compiledUrlPatterns = reusePatterns?.length ? compileUrlPatterns(reusePatterns) : [];\n\n          await executeCritical(\n            async () => this.refreshPrimaryPage(),\n            (error) =>\n              new BrowserProcessError(\n                \"Failed to refresh primary page when reusing browser instance\",\n                BrowserProcessState.PAGE_REFRESH,\n                error,\n              ),\n          );\n\n          // Session context injection - should throw error if it fails\n          if (this.launchConfig?.sessionContext) {\n            this.logger.debug(\n              `[CDPService] Session created with session context, injecting session context`,\n            );\n            await executeCritical(\n              async () =>\n                this.injectSessionContext(this.primaryPage!, this.launchConfig!.sessionContext!),\n              (error) => {\n                const contextError = new SessionContextError(\n                  error instanceof Error ? error.message : String(error),\n                  SessionContextType.CONTEXT_INJECTION,\n                  error,\n                );\n                this.logger.warn(`[CDPService] ${contextError.message} - throwing error`);\n                return contextError;\n              },\n            );\n          }\n          await this.pluginManager.onBrowserReady(this.launchConfig);\n\n          return this.browserInstance!;\n        } else if (this.browserInstance) {\n          this.logger.info(\n            \"[CDPService] Existing browser instance detected. Closing it before launching a new one.\",\n          );\n          await executeBestEffort(\n            this.logger,\n            async () => this.shutdown(),\n            \"Error during shutdown before launch\",\n          );\n        }\n\n        this.launchConfig = config || this.defaultLaunchConfig;\n\n        const optimize = this.launchConfig.optimizeBandwidth;\n        const rawPatterns = typeof optimize === \"object\" ? optimize.blockUrlPatterns : undefined;\n        this.compiledUrlPatterns = rawPatterns?.length ? compileUrlPatterns(rawPatterns) : [];\n\n        this.logger.info(\"[CDPService] Launching new browser instance.\");\n\n        // Validate configuration\n        await executeCritical(\n          async () => validateLaunchConfig(this.launchConfig!),\n          (error) => categorizeError(error, \"configuration validation\"),\n        );\n\n        // File cleanup - non-critical, log errors but continue\n        this.logger.info(\"[CDPService] Cleaning up files before browser launch\");\n        await executeOptional(\n          this.logger,\n          async () => {\n            await FileService.getInstance().cleanupFiles();\n            this.logger.info(\"[CDPService] Files cleaned successfully before launch\");\n          },\n          (error) =>\n            new CleanupError(\n              error instanceof Error ? error.message : String(error),\n              CleanupType.PRE_LAUNCH_FILE_CLEANUP,\n              error,\n            ),\n        );\n\n        const { options, userAgent, userDataDir, fingerprint } = this.launchConfig;\n        this.fingerprintData = fingerprint ?? null;\n\n        // Run launch mutators - plugin errors should be caught\n        await executeCritical(\n          async () => {\n            for (const mutator of this.launchMutators) {\n              await mutator(this.launchConfig!);\n            }\n          },\n          (error) =>\n            new PluginError(\n              error instanceof Error ? error.message : String(error),\n              PluginName.LAUNCH_MUTATOR,\n              PluginOperation.PRE_LAUNCH_HOOK,\n              true,\n              error,\n            ),\n        );\n\n        // Fingerprint generation - can fail gracefully\n        if (\n          !env.SKIP_FINGERPRINT_INJECTION &&\n          !userAgent &&\n          !this.launchConfig.skipFingerprintInjection &&\n          !this.fingerprintData\n        ) {\n          await executeCritical(\n            async () => {\n              let fingerprintOptions: Partial<FingerprintGeneratorOptions> = {\n                devices: [\"desktop\"],\n                operatingSystems: [\"linux\"],\n                browsers: [{ name: \"chrome\", minVersion: 136 }],\n                locales: [\"en-US\", \"en\"],\n                screen: {\n                  minWidth: this.launchConfig!.dimensions?.width ?? 1920,\n                  minHeight: this.launchConfig!.dimensions?.height ?? 1080,\n                  maxWidth: this.launchConfig!.dimensions?.width ?? 1920,\n                  maxHeight: this.launchConfig!.dimensions?.height ?? 1080,\n                },\n              };\n\n              if (this.launchConfig!.deviceConfig?.device === \"mobile\") {\n                fingerprintOptions = {\n                  devices: [\"mobile\"],\n                  locales: [\"en-US\", \"en\"],\n                };\n              }\n\n              const fingerprintGen = new FingerprintGenerator(fingerprintOptions);\n              this.fingerprintData = fingerprintGen.getFingerprint();\n            },\n            (error) => {\n              this.logger.error({ err: error }, \"[CDPService] Error generating fingerprint\");\n              return new FingerprintError(\n                error instanceof Error ? error.message : String(error),\n                FingerprintStage.GENERATION,\n                error,\n              );\n            },\n          );\n        } else if (this.fingerprintData) {\n          this.logger.info(\n            `[CDPService] Using existing fingerprint with user agent: ${this.fingerprintData.fingerprint.navigator.userAgent}`,\n          );\n        }\n\n        const isHeadless = !!this.launchConfig?.options?.headless;\n\n        this.currentSessionConfig = {\n          ...this.launchConfig,\n          dimensions: this.launchConfig.dimensions || this.fingerprintData?.fingerprint.screen,\n          userAgent:\n            this.launchConfig.userAgent || this.fingerprintData?.fingerprint.navigator.userAgent,\n        };\n\n        const extensionPaths = await executeCritical(\n          async () => {\n            const defaultExtensions = isHeadless ? [\"recorder\"] : [];\n            const customExtensions = this.launchConfig!.extensions\n              ? [...this.launchConfig!.extensions]\n              : [];\n\n            // Get named extension paths\n            const namedExtensionPaths = await getExtensionPaths([\n              ...defaultExtensions,\n              ...customExtensions,\n            ]);\n\n            // Check for session extensions passed from the API\n            let sessionExtensionPaths: string[] = [];\n            if (this.launchConfig!.extra?.orgExtensions?.paths) {\n              sessionExtensionPaths = this.launchConfig!.extra.orgExtensions\n                .paths as unknown as string[];\n              this.logger.info(\n                `[CDPService] Found ${sessionExtensionPaths.length} session extension paths`,\n              );\n            }\n\n            return [...namedExtensionPaths, ...sessionExtensionPaths];\n          },\n          (error) =>\n            new ResourceError(\n              `Failed to resolve extension paths: ${error}`,\n              ResourceType.EXTENSIONS,\n              false,\n              error,\n            ),\n        );\n\n        let timezone = this.defaultTimezone;\n        if (config?.timezone) {\n          const validatedTimezone = await executeOptional(\n            this.logger,\n            async () => {\n              if (this.launchConfig?.skipFingerprintInjection) {\n                this.logger.info(\n                  `Skipping timezone validation as skipFingerprintInjection is enabled`,\n                );\n                return this.defaultTimezone;\n              }\n              const tz = await validateTimezone(this.logger, config.timezone!);\n              this.logger.info(`Resolved and validated timezone: ${tz}`);\n              return tz;\n            },\n            (error) => {\n              this.logger.warn(`Timezone validation failed, using fallback`);\n              return categorizeError(error, \"timezone validation\");\n            },\n            this.defaultTimezone,\n          );\n          timezone = validatedTimezone ?? this.defaultTimezone;\n        }\n\n        const extensionArgs = extensionPaths.length\n          ? [\n              `--load-extension=${extensionPaths.join(\",\")}`,\n              `--disable-extensions-except=${extensionPaths.join(\",\")}`,\n            ]\n          : [];\n\n        const shouldDisableSandbox =\n          env.DISABLE_CHROME_SANDBOX ||\n          (typeof process.getuid === \"function\" && process.getuid() === 0);\n\n        const staticDefaultArgs = [\n          \"--remote-allow-origins=*\",\n          \"--disable-dev-shm-usage\",\n          \"--disable-gpu\",\n          \"--disable-features=TranslateUI,BlinkGenPropertyTrees,LinuxNonClientFrame,PermissionPromptSurvey,IsolateOrigins,site-per-process,TouchpadAndWheelScrollLatching,TrackingProtection3pcd,InterestFeedContentSuggestions,PrivacySandboxSettings4,AutofillServerCommunication,OptimizationHints,MediaRouter,DialMediaRouteProvider,CertificateTransparencyComponentUpdater,GlobalMediaControls,AudioServiceOutOfProcess,LazyFrameLoading,AvoidUnnecessaryBeforeUnloadCheckSync\",\n          \"--enable-features=Clipboard\",\n          \"--no-default-browser-check\",\n          \"--disable-sync\",\n          \"--disable-translate\",\n          \"--no-first-run\",\n          \"--disable-search-engine-choice-screen\",\n          \"--webrtc-ip-handling-policy=disable_non_proxied_udp\",\n          \"--force-webrtc-ip-handling-policy\",\n          \"--disable-touch-editing\",\n          \"--disable-touch-drag-drop\",\n          \"--disable-client-side-phishing-detection\",\n          \"--disable-default-apps\",\n          \"--disable-component-update\",\n          \"--disable-infobars\",\n          \"--disable-breakpad\",\n          \"--disable-background-networking\",\n          \"--disable-session-crashed-bubble\",\n          \"--disable-ipc-flooding-protection\",\n          \"--disable-popup-blocking\",\n          \"--disable-prompt-on-repost\",\n          \"--disable-domain-reliability\",\n          \"--metrics-recording-only\",\n          \"--no-pings\",\n          \"--disable-backing-store-limit\",\n          \"--password-store=basic\",\n          ...(shouldDisableSandbox\n            ? [\"--no-sandbox\", \"--disable-setuid-sandbox\", \"--no-zygote\"]\n            : []),\n        ];\n\n        const headfulArgs = [\n          \"--ozone-platform=x11\",\n          \"--disable-renderer-backgrounding\",\n          \"--disable-backgrounding-occluded-windows\",\n          \"--use-gl=swiftshader\",\n          \"--in-process-gpu\",\n          \"--enable-crashpad\",\n          \"--crash-dumps-dir=/tmp/chrome-dumps\",\n          \"--noerrdialogs\",\n          \"--force-device-scale-factor=1\",\n          \"--disable-hang-monitor\",\n        ];\n\n        const headlessArgs = [\n          \"--headless=new\",\n          \"--hide-crash-restore-bubble\",\n          \"--disable-blink-features=AutomationControlled\",\n          // can we just remove this outright?\n          `--unsafely-treat-insecure-origin-as-secure=http://localhost:3000,http://${env.HOST}:${env.PORT}`,\n        ];\n\n        const dynamicArgs = [\n          this.launchConfig.dimensions ? \"\" : \"--start-maximized\",\n          `--remote-debugging-address=${env.HOST}`,\n          \"--remote-debugging-port=9222\",\n          `--window-size=${this.launchConfig.dimensions?.width ?? 1920},${\n            this.launchConfig.dimensions?.height ?? 1080\n          }`,\n          userAgent ? `--user-agent=${userAgent}` : \"\",\n          this.launchConfig.options.proxyUrl\n            ? `--proxy-server=${this.launchConfig.options.proxyUrl}`\n            : \"\",\n        ];\n\n        const uniq = (xs: string[]) => Array.from(new Set(xs.filter(Boolean)));\n\n        const launchArgs = uniq([\n          ...staticDefaultArgs,\n          ...(isHeadless ? headlessArgs : headfulArgs),\n          ...dynamicArgs,\n          ...extensionArgs,\n          ...(options.args || []),\n          ...(env.CHROME_ARGS || []),\n        ]).filter((arg) => !env.FILTER_CHROME_ARGS.includes(arg));\n\n        const finalLaunchOptions = {\n          ...options,\n          defaultViewport: null,\n          args: launchArgs,\n          executablePath: this.chromeExecPath,\n          ignoreDefaultArgs: [\"--enable-automation\"],\n          timeout: 0,\n          env: {\n            HOME: os.userInfo().homedir,\n            TZ: timezone,\n            ...(isHeadless ? {} : { DISPLAY: env.DISPLAY }),\n          },\n          userDataDir,\n          dumpio: env.DEBUG_CHROME_PROCESS, // Enable Chrome process stdout and stderr\n        };\n\n        this.logger.info(`[CDPService] Launch Options:`);\n        this.logger.info(JSON.stringify(finalLaunchOptions, null, 2));\n\n        if (userDataDir && this.launchConfig.userPreferences) {\n          this.logger.info(`[CDPService] Setting up user preferences in ${userDataDir}`);\n          await executeBestEffort(\n            this.logger,\n            async () => this.setupUserPreferences(userDataDir, this.launchConfig!.userPreferences!),\n            \"Failed to set up user preferences\",\n          );\n        }\n\n        // Browser process launch - most critical step\n        this.browserInstance = await executeCritical(\n          async () =>\n            (await tracer.startActiveSpan(\"CDPService.launchBrowser\", async () => {\n              return await puppeteer.launch(finalLaunchOptions);\n            })) as unknown as Browser,\n          (error) =>\n            new BrowserProcessError(\n              error instanceof Error ? error.message : String(error),\n              BrowserProcessState.LAUNCH_FAILED,\n              error,\n            ),\n        );\n\n        // Plugin notifications - catch individual plugin errors\n        await executeOptional(\n          this.logger,\n          async () => this.pluginManager.onBrowserLaunch(this.browserInstance!),\n          (error) =>\n            new PluginError(\n              error instanceof Error ? error.message : String(error),\n              PluginName.PLUGIN_MANAGER,\n              PluginOperation.BROWSER_LAUNCH_NOTIFICATION,\n              true,\n              error,\n            ),\n        );\n\n        this.browserInstance.on(\"error\", (err) => {\n          this.logger.error(`[CDPService] Browser error: ${err}`);\n          const error = err as Error;\n          this.instrumentationLogger.record({\n            type: BrowserEventType.BrowserError,\n            error: { message: error?.message, stack: error?.stack },\n            timestamp: new Date().toISOString(),\n          });\n        });\n\n        this.primaryPage = await executeCritical(\n          async () => (await this.browserInstance!.pages())[0],\n          (error) =>\n            new BrowserProcessError(\n              \"Failed to get primary page from browser instance\",\n              BrowserProcessState.PAGE_ACCESS,\n              error,\n            ),\n        );\n\n        // Session context injection - should throw error if it fails\n        if (this.launchConfig?.sessionContext) {\n          this.logger.debug(\n            `[CDPService] Session created with session context, injecting session context`,\n          );\n          await executeCritical(\n            async () =>\n              this.injectSessionContext(this.primaryPage!, this.launchConfig!.sessionContext!),\n            (error) => {\n              const contextError = new SessionContextError(\n                error instanceof Error ? error.message : String(error),\n                SessionContextType.CONTEXT_INJECTION,\n                error,\n              );\n              this.logger.warn(`[CDPService] ${contextError.message} - throwing error`);\n              return contextError;\n            },\n          );\n        }\n\n        // Configure browser download behavior\n        await executeBestEffort(\n          this.logger,\n          async () => {\n            const downloadPath = FileService.getInstance().getBaseFilesPath();\n            const cdpSession = await this.browserInstance!.target().createCDPSession();\n            await cdpSession.send(\"Browser.setDownloadBehavior\", {\n              behavior: \"allow\",\n              downloadPath: downloadPath,\n              eventsEnabled: true,\n            });\n            await cdpSession.detach();\n            this.logger.debug(\n              `[CDPService] Download behavior configured with path: ${downloadPath}`,\n            );\n          },\n          \"Failed to configure download behavior\",\n        );\n\n        this.browserInstance.on(\"targetcreated\", this.handleNewTarget.bind(this));\n        this.browserInstance.on(\"targetchanged\", this.handleTargetChange.bind(this));\n        this.browserInstance.on(\"targetdestroyed\", (target) => {\n          const targetId = (target as any)._targetId;\n          this.targetInstrumentationManager.detach(targetId);\n        });\n        this.browserInstance.on(\"disconnected\", this.onDisconnect.bind(this));\n\n        this.wsEndpoint = await executeCritical(\n          async () => this.browserInstance!.wsEndpoint(),\n          (error) =>\n            new NetworkError(\n              \"Failed to get WebSocket endpoint from browser\",\n              NetworkOperation.WEBSOCKET_SETUP,\n              error,\n            ),\n        );\n\n        // Final setup steps\n        await executeOptional(\n          this.logger,\n          async () => {\n            await this.handleNewTarget(this.primaryPage!.target());\n            await this.handleTargetChange(this.primaryPage!.target());\n          },\n          (error) =>\n            new BrowserProcessError(\n              error instanceof Error ? error.message : String(error),\n              BrowserProcessState.TARGET_SETUP,\n              error,\n            ),\n        );\n\n        try {\n          const existingTargets = await this.browserInstance.targets();\n          for (const target of existingTargets) {\n            if ((target as any)._targetId !== (this.primaryPage.target() as any)._targetId) {\n              await this.targetInstrumentationManager.attach(target, target.type() as TargetType);\n            }\n          }\n          this.logger.info(\n            `[CDPService] Attached instrumentation to ${existingTargets.length} existing targets`,\n          );\n        } catch (error) {\n          this.logger.error({ err: error }, `[CDPService] Error attaching to existing targets`);\n        }\n\n        await this.pluginManager.onBrowserReady(this.launchConfig);\n\n        return this.browserInstance;\n      })();\n\n      return (await Promise.race([launchProcess, launchTimeout])) as Browser;\n    } catch (error: unknown) {\n      const categorizedError =\n        error instanceof BaseLaunchError ? error : categorizeError(error, \"browser launch\");\n\n      this.logger.error(\n        {\n          error: {\n            errorType: categorizedError.type,\n            isRetryable: categorizedError.isRetryable,\n            context: categorizedError.context,\n          },\n        },\n        `[CDPService] LAUNCH ERROR (${categorizedError.type}): ${categorizedError.message}`,\n      );\n\n      throw categorizedError;\n    }\n  }\n\n  @traceable\n  public async proxyWebSocket(req: IncomingMessage, socket: Duplex, head: Buffer): Promise<void> {\n    if (this.proxyWebSocketHandler) {\n      this.logger.info(\"[CDPService] Using custom WebSocket proxy handler\");\n      await this.proxyWebSocketHandler(req, socket, head);\n      return;\n    }\n\n    if (!this.wsEndpoint) {\n      throw new Error(`WebSocket endpoint not available. Ensure the browser is launched first.`);\n    }\n\n    const cleanupListeners = () => {\n      this.browserInstance?.off(\"close\", cleanupListeners);\n      if (this.browserInstance?.process()) {\n        this.browserInstance.process()?.off(\"close\", cleanupListeners);\n      }\n      this.browserInstance?.off(\"disconnected\", cleanupListeners);\n      socket.off(\"close\", cleanupListeners);\n      socket.off(\"error\", cleanupListeners);\n      this.logger.info(\"[CDPService] WebSocket connection listeners cleaned up\");\n    };\n\n    this.browserInstance?.once(\"close\", cleanupListeners);\n    if (this.browserInstance?.process()) {\n      this.browserInstance.process()?.once(\"close\", cleanupListeners);\n    }\n    this.browserInstance?.once(\"disconnected\", cleanupListeners);\n    socket.once(\"close\", cleanupListeners);\n    socket.once(\"error\", cleanupListeners);\n\n    // Increase max listeners\n    if (this.browserInstance?.process()) {\n      this.browserInstance.process()!.setMaxListeners(60);\n    }\n\n    this.wsProxyServer.ws(\n      req,\n      socket,\n      head,\n      {\n        target: this.wsEndpoint,\n      },\n      (error) => {\n        if (error) {\n          this.logger.error(`WebSocket proxy error: ${error}`);\n          cleanupListeners(); // Clean up on error too\n        }\n      },\n    );\n\n    socket.on(\"error\", (error) => {\n      this.logger.error(`Socket error: ${error}`);\n      // Try to end the socket properly on error\n      try {\n        socket.end();\n      } catch (e) {\n        this.logger.error(`Error ending socket: ${e}`);\n      }\n    });\n  }\n\n  public getUserAgent() {\n    return (\n      this.currentSessionConfig?.userAgent || this.fingerprintData?.fingerprint.navigator.userAgent\n    );\n  }\n\n  public getDimensions() {\n    return this.currentSessionConfig?.dimensions || { width: 1920, height: 1080 };\n  }\n\n  public getFingerprintData(): BrowserFingerprintWithHeaders | null {\n    return this.fingerprintData;\n  }\n\n  public async getCookies(): Promise<Protocol.Network.Cookie[]> {\n    if (!this.primaryPage) {\n      throw new Error(\"Primary page not initialized\");\n    }\n    const client = await this.primaryPage.createCDPSession();\n    const { cookies } = await client.send(\"Network.getAllCookies\");\n    await client.detach();\n    return cookies;\n  }\n\n  public async getBrowserState(): Promise<SessionData> {\n    if (!this.browserInstance || !this.primaryPage) {\n      throw new Error(\"Browser or primary page not initialized\");\n    }\n\n    const userDataDir = this.launchConfig?.userDataDir;\n\n    if (!userDataDir) {\n      this.logger.warn(\"No userDataDir specified, returning empty session data\");\n      return {};\n    }\n\n    try {\n      this.logger.info(`[CDPService] Dumping session data from userDataDir: ${userDataDir}`);\n\n      // Run session data extraction and CDP storage extraction in parallel\n      const [cookieData, sessionData, storageData] = await Promise.all([\n        this.getCookies(),\n        this.chromeSessionService.getSessionData(userDataDir),\n        this.getExistingPageSessionData(),\n      ]);\n\n      // Merge storage data with session data\n      const result = {\n        cookies: cookieData,\n        localStorage: {\n          ...(sessionData.localStorage || {}),\n          ...(storageData.localStorage || {}),\n        },\n        sessionStorage: {\n          ...(sessionData.sessionStorage || {}),\n          ...(storageData.sessionStorage || {}),\n        },\n        indexedDB: {\n          ...(sessionData.indexedDB || {}),\n          ...(storageData.indexedDB || {}),\n        },\n      };\n\n      this.logger.info(\"[CDPService] Session data dumped successfully\");\n      return result;\n    } catch (error) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      this.logger.error(`[CDPService] Error dumping session data: ${errorMessage}`);\n      return {};\n    }\n  }\n\n  /**\n   * Extract all storage data (localStorage, sessionStorage, IndexedDB) for all open pages\n   */\n  private async getExistingPageSessionData(): Promise<SessionData> {\n    if (!this.browserInstance || !this.primaryPage) {\n      return {};\n    }\n\n    const result: SessionData = {\n      localStorage: {},\n      sessionStorage: {},\n      indexedDB: {},\n    };\n\n    try {\n      const pages = await this.browserInstance.pages();\n\n      const validPages = pages.filter((page) => {\n        try {\n          const url = page.url();\n          return url && url.startsWith(\"http\");\n        } catch (e) {\n          return false;\n        }\n      });\n\n      this.logger.info(\n        `[CDPService] Processing ${validPages.length} valid pages out of ${pages.length} total for storage extraction`,\n      );\n\n      const results = await Promise.all(\n        validPages.map((page) => extractStorageForPage(page, this.logger)),\n      );\n\n      // Merge all results\n      for (const item of results) {\n        for (const domain in item.localStorage) {\n          result.localStorage![domain] = {\n            ...(result.localStorage![domain] || {}),\n            ...item.localStorage![domain],\n          };\n        }\n\n        for (const domain in item.sessionStorage) {\n          result.sessionStorage![domain] = {\n            ...(result.sessionStorage![domain] || {}),\n            ...item.sessionStorage![domain],\n          };\n        }\n\n        for (const domain in item.indexedDB) {\n          result.indexedDB![domain] = [\n            ...(result.indexedDB![domain] || []),\n            ...item.indexedDB![domain],\n          ];\n        }\n      }\n\n      return result;\n    } catch (error) {\n      this.logger.error(`[CDPService] Error extracting storage with CDP: ${error}`);\n      return result;\n    }\n  }\n\n  public async getAllPages() {\n    return this.browserInstance?.pages() || [];\n  }\n\n  @traceable\n  public async startNewSession(sessionConfig: BrowserLauncherOptions): Promise<Browser> {\n    this.currentSessionConfig = sessionConfig;\n    this.trackedOrigins.clear(); // Clear tracked origins when starting a new session\n\n    // Recreate target instrumentation manager with session-specific options\n    this.targetInstrumentationManager = new TargetInstrumentationManager(\n      this.instrumentationLogger,\n      this.logger,\n      { dangerouslyLogRequestDetails: sessionConfig.dangerouslyLogRequestDetails },\n    );\n\n    return this.launch(sessionConfig);\n  }\n\n  @traceable\n  public async endSession(): Promise<void> {\n    this.logger.info(\"Ending current session and resetting to default configuration.\");\n    const sessionConfig = this.currentSessionConfig!;\n\n    this.sessionContext = await this.getBrowserState().catch(() => null);\n\n    await this.shutdown();\n    await this.pluginManager.onSessionEnd(sessionConfig);\n    this.currentSessionConfig = null;\n    this.sessionContext = null;\n    this.trackedOrigins.clear();\n\n    this.instrumentationLogger.resetContext();\n\n    // Reset target instrumentation manager to clear session-specific options\n    // (e.g. dangerouslyLogRequestDetails) so they don't leak into the idle browser\n    this.targetInstrumentationManager = new TargetInstrumentationManager(\n      this.instrumentationLogger,\n      this.logger,\n    );\n\n    await this.launch(this.defaultLaunchConfig);\n  }\n\n  private async onDisconnect(): Promise<void> {\n    this.logger.info(\"Browser disconnected. Handling cleanup.\");\n\n    if (this.shuttingDown) {\n      return;\n    }\n\n    await this.disconnectHandler();\n  }\n\n  @traceable\n  private async injectSessionContext(\n    page: Page,\n    context?: BrowserLauncherOptions[\"sessionContext\"],\n  ) {\n    if (!context) return;\n\n    const storageByOrigin = groupSessionStorageByOrigin(context);\n\n    for (const origin of storageByOrigin.keys()) {\n      this.trackedOrigins.add(origin);\n    }\n\n    const client = await page.createCDPSession();\n    try {\n      if (context.cookies?.length) {\n        await client.send(\"Network.setCookies\", {\n          cookies: context.cookies.map((cookie) => ({\n            ...cookie,\n            partitionKey: cookie.partitionKey as unknown as Protocol.Network.Cookie[\"partitionKey\"],\n          })),\n        });\n        this.logger.info(`[CDPService] Set ${context.cookies.length} cookies`);\n      }\n    } catch (error) {\n      this.logger.error(`[CDPService] Error setting cookies: ${error}`);\n    } finally {\n      await client.detach().catch(() => {});\n    }\n\n    this.logger.info(\n      `[CDPService] Registered frame navigation handler for ${storageByOrigin.size} origins`,\n    );\n    page.on(\"framenavigated\", (frame) => handleFrameNavigated(frame, storageByOrigin, this.logger));\n\n    page.browser().on(\"targetcreated\", async (target) => {\n      if (target.type() === \"page\") {\n        try {\n          const newPage = await target.page();\n          if (newPage) {\n            newPage.on(\"framenavigated\", (frame) =>\n              handleFrameNavigated(frame, storageByOrigin, this.logger),\n            );\n          }\n        } catch (err) {\n          this.logger.error(`[CDPService] Error adding framenavigated handler to new page: ${err}`);\n        }\n      }\n    });\n\n    this.logger.debug(\"[CDPService] Session context injection setup complete\");\n  }\n\n  @traceable\n  private async injectFingerprintSafely(\n    page: Page,\n    fingerprintData: BrowserFingerprintWithHeaders | null,\n  ) {\n    if (!fingerprintData) return;\n\n    try {\n      const { fingerprint, headers } = fingerprintData;\n      // TypeScript fix - access userAgent through navigator property\n      const userAgent = fingerprint.navigator.userAgent;\n      const userAgentMetadata = fingerprint.navigator.userAgentData;\n      const { screen } = fingerprint;\n\n      await page.setUserAgent(userAgent);\n\n      const session = await page.createCDPSession();\n\n      try {\n        await session.send(\"Page.setDeviceMetricsOverride\", {\n          screenHeight: screen.height,\n          screenWidth: screen.width,\n          width: screen.width,\n          height: screen.height,\n          viewport: {\n            width: screen.availWidth,\n            height: screen.availHeight,\n            scale: 1,\n            x: 0,\n            y: 0,\n          },\n          mobile: /phone|android|mobile/i.test(userAgent),\n          screenOrientation:\n            screen.height > screen.width\n              ? { angle: 0, type: \"portraitPrimary\" }\n              : { angle: 90, type: \"landscapePrimary\" },\n          deviceScaleFactor: screen.devicePixelRatio,\n        });\n\n        const injectedHeaders = filterHeaders(headers);\n\n        await page.setExtraHTTPHeaders(injectedHeaders);\n\n        await session.send(\"Emulation.setUserAgentOverride\", {\n          userAgent: userAgent,\n          acceptLanguage: headers[\"accept-language\"],\n          platform: fingerprint.navigator.platform || \"Linux x86_64\",\n          userAgentMetadata: {\n            brands:\n              userAgentMetadata.brands as unknown as Protocol.Emulation.UserAgentMetadata[\"brands\"],\n            fullVersionList:\n              userAgentMetadata.fullVersionList as unknown as Protocol.Emulation.UserAgentMetadata[\"fullVersionList\"],\n            fullVersion: userAgentMetadata.uaFullVersion,\n            platform: fingerprint.navigator.platform || \"Linux x86_64\",\n            platformVersion: userAgentMetadata.platformVersion || \"\",\n            architecture: userAgentMetadata.architecture || \"x86\",\n            model: userAgentMetadata.model || \"\",\n            mobile: userAgentMetadata.mobile as unknown as boolean,\n            bitness: userAgentMetadata.bitness || \"64\",\n            wow64: false, // wow64 property doesn't exist on UserAgentData, defaulting to false\n          },\n        });\n      } finally {\n        // Always detach the session when done\n        await session.detach().catch(() => {});\n      }\n\n      await page.evaluateOnNewDocument(\n        loadFingerprintScript({\n          fixedPlatform: fingerprint.navigator.platform || \"Linux x86_64\",\n          fixedVendor: (fingerprint.videoCard as VideoCard | null)?.vendor,\n          fixedRenderer: (fingerprint.videoCard as VideoCard | null)?.renderer,\n          fixedDeviceMemory: fingerprint.navigator.deviceMemory || 8,\n          fixedHardwareConcurrency: fingerprint.navigator.hardwareConcurrency || 8,\n          fixedArchitecture: userAgentMetadata.architecture || \"x86\",\n          fixedBitness: userAgentMetadata.bitness || \"64\",\n          fixedModel: userAgentMetadata.model || \"\",\n          fixedPlatformVersion: userAgentMetadata.platformVersion || \"15.0.0\",\n          fixedUaFullVersion: userAgentMetadata.uaFullVersion || \"131.0.6778.86\",\n          fixedBrands:\n            userAgentMetadata.brands ||\n            ([] as unknown as Array<{\n              brand: string;\n              version: string;\n            }>),\n        }),\n      );\n    } catch (error) {\n      this.logger.error({ error }, `[Fingerprint] Error injecting fingerprint safely`);\n      const fingerprintInjector = new FingerprintInjector();\n      // @ts-ignore - Ignore type mismatch between puppeteer versions\n      await fingerprintInjector.attachFingerprintToPuppeteer(page, fingerprintData);\n    }\n  }\n\n  @traceable\n  private async setupUserPreferences(userDataDir: string, userPreferences: Record<string, any>) {\n    try {\n      const preferencesPath = getProfilePath(userDataDir, \"Preferences\");\n      const defaultProfileDir = path.dirname(preferencesPath);\n\n      await fs.promises.mkdir(defaultProfileDir, { recursive: true });\n\n      let existingPreferences = {};\n\n      try {\n        const existingContent = await fs.promises.readFile(preferencesPath, \"utf8\");\n        existingPreferences = JSON.parse(existingContent);\n      } catch (error) {\n        this.logger.debug(`[CDPService] No existing preferences found, creating new: ${error}`);\n      }\n\n      const mergedPreferences = deepMerge(existingPreferences, userPreferences);\n\n      await fs.promises.writeFile(preferencesPath, JSON.stringify(mergedPreferences, null, 2));\n\n      this.logger.info(`[CDPService] User preferences written to ${preferencesPath}`);\n    } catch (error) {\n      this.logger.error(`[CDPService] Error setting up user preferences: ${error}`);\n      throw error;\n    }\n  }\n}\n"
  },
  {
    "path": "api/src/services/cdp/errors/launch-errors.ts",
    "content": "/**\n * Custom error classes for categorizing CDPService launch failures\n * These allow for slightly more intelligent error handling and recovery strategies\n */\n\nexport enum LaunchErrorType {\n  TIMEOUT = \"TIMEOUT\",\n  CONFIGURATION = \"CONFIGURATION\",\n  RESOURCE = \"RESOURCE\",\n  SYSTEM = \"SYSTEM\",\n  NETWORK = \"NETWORK\",\n  FINGERPRINT = \"FINGERPRINT\",\n  PLUGIN = \"PLUGIN\",\n  CLEANUP = \"CLEANUP\",\n  BROWSER_PROCESS = \"BROWSER_PROCESS\",\n  SESSION_CONTEXT = \"SESSION_CONTEXT\",\n}\n\nexport enum BrowserProcessState {\n  PAGE_REFRESH = \"page_refresh\",\n  LAUNCH_FAILED = \"launch_failed\",\n  PAGE_ACCESS = \"page_access\",\n  TARGET_SETUP = \"target_setup\",\n  UNKNOWN = \"unknown\",\n}\n\nexport enum PluginName {\n  LAUNCH_MUTATOR = \"launch_mutator\",\n  PLUGIN_MANAGER = \"plugin_manager\",\n  UNKNOWN = \"unknown\",\n}\n\nexport enum PluginOperation {\n  PRE_LAUNCH_HOOK = \"pre-launch hook\",\n  BROWSER_LAUNCH_NOTIFICATION = \"browser launch notification\",\n  LAUNCH = \"launch\",\n}\n\nexport enum CleanupType {\n  PRE_LAUNCH_FILE_CLEANUP = \"pre-launch file cleanup\",\n  GENERAL = \"general\",\n}\n\nexport enum SessionContextType {\n  CONTEXT_INJECTION = \"context injection\",\n}\n\nexport enum FingerprintStage {\n  GENERATION = \"generation\",\n  INJECTION = \"injection\",\n}\n\nexport enum ResourceType {\n  EXTENSIONS = \"extensions\",\n  FILE = \"file\",\n}\n\nexport enum NetworkOperation {\n  WEBSOCKET_SETUP = \"websocket setup\",\n  PORT_BINDING = \"port binding\",\n  NETWORK_SETUP = \"network setup\",\n}\n\nexport enum SystemOperation {\n  FILE_ACCESS = \"file access\",\n  UNKNOWN_OPERATION = \"unknown operation\",\n}\n\nexport enum ConfigurationField {\n  DIMENSIONS = \"dimensions\",\n  TIMEZONE = \"timezone\",\n  PROXY_URL = \"proxyUrl\",\n}\n\nexport enum ErrorCategories {}\n\nexport abstract class BaseLaunchError extends Error {\n  public readonly type: LaunchErrorType;\n  public readonly isRetryable: boolean;\n  public readonly context?: Record<string, any>;\n\n  constructor(\n    type: LaunchErrorType,\n    message: string,\n    isRetryable: boolean = false,\n    context?: Record<string, any>,\n    cause?: unknown,\n  ) {\n    super(message, { cause });\n    this.name = this.constructor.name;\n    this.type = type;\n    this.isRetryable = isRetryable;\n    this.context = context;\n\n    // Maintains proper stack trace for where error was thrown (only available on V8)\n    if (Error.captureStackTrace) {\n      Error.captureStackTrace(this, this.constructor);\n    }\n  }\n}\n\n/**\n * Thrown when browser launch times out after 30 seconds\n * This is typically retryable as it may be a temporary resource issue\n */\nexport class LaunchTimeoutError extends BaseLaunchError {\n  constructor(timeoutMs: number = 30000, cause?: unknown) {\n    super(\n      LaunchErrorType.TIMEOUT,\n      `Browser launch timeout after ${timeoutMs}ms`,\n      true,\n      {\n        timeoutMs,\n      },\n      cause,\n    );\n  }\n}\n\n/**\n * Thrown when configuration parameters are invalid or incompatible\n * These are typically not retryable without fixing the configuration\n */\nexport class ConfigurationError extends BaseLaunchError {\n  constructor(\n    message: string,\n    configField?: ConfigurationField,\n    configValue?: any,\n    cause?: unknown,\n  ) {\n    super(\n      LaunchErrorType.CONFIGURATION,\n      `Configuration error: ${message}`,\n      false,\n      {\n        configField,\n        configValue,\n      },\n      cause,\n    );\n  }\n}\n\n/**\n * Thrown when required system resources are unavailable\n * Some may be retryable (temporary disk space), others not (missing Chrome binary)\n */\nexport class ResourceError extends BaseLaunchError {\n  constructor(\n    message: string,\n    resourceType: ResourceType,\n    isRetryable: boolean = false,\n    cause?: unknown,\n  ) {\n    super(\n      LaunchErrorType.RESOURCE,\n      `Resource error: ${message}`,\n      isRetryable,\n      { resourceType },\n      cause,\n    );\n  }\n}\n\n/**\n * Thrown when system-level operations fail\n * Usually retryable as they may be temporary system issues\n */\nexport class SystemError extends BaseLaunchError {\n  constructor(message: string, operation: SystemOperation, originalError?: Error) {\n    super(\n      LaunchErrorType.SYSTEM,\n      `System error during ${operation}: ${message}`,\n      true,\n      {\n        operation,\n        originalError: originalError?.message,\n      },\n      originalError,\n    );\n  }\n}\n\n/**\n * Thrown when network-related operations fail (proxy, WebSocket setup)\n * Usually retryable as network issues are often temporary\n */\nexport class NetworkError extends BaseLaunchError {\n  constructor(message: string, networkOperation: NetworkOperation, cause?: unknown) {\n    super(\n      LaunchErrorType.NETWORK,\n      `Network error during ${networkOperation}: ${message}`,\n      true,\n      {\n        networkOperation,\n      },\n      cause,\n    );\n  }\n}\n\n/**\n * Thrown when fingerprint generation or injection fails\n * Usually retryable, can fall back to no fingerprint\n */\nexport class FingerprintError extends BaseLaunchError {\n  constructor(message: string, stage: FingerprintStage, cause?: unknown) {\n    super(\n      LaunchErrorType.FINGERPRINT,\n      `Fingerprint error during ${stage}: ${message}`,\n      true,\n      {\n        stage,\n      },\n      cause,\n    );\n  }\n}\n\n/**\n * Thrown when plugin operations fail during launch\n * May or may not be retryable depending on the plugin\n */\nexport class PluginError extends BaseLaunchError {\n  constructor(\n    message: string,\n    pluginName: PluginName,\n    operation: PluginOperation,\n    isRetryable: boolean = true,\n    cause?: unknown,\n  ) {\n    super(\n      LaunchErrorType.PLUGIN,\n      `Plugin error in ${pluginName} during ${operation}: ${message}`,\n      isRetryable,\n      {\n        pluginName,\n        operation,\n      },\n      cause,\n    );\n  }\n}\n\n/**\n * Thrown when file cleanup operations fail\n * Usually retryable and non-critical to browser launch\n */\nexport class CleanupError extends BaseLaunchError {\n  constructor(message: string, cleanupType: CleanupType, cause?: unknown) {\n    super(\n      LaunchErrorType.CLEANUP,\n      `Cleanup error during ${cleanupType}: ${message}`,\n      true,\n      {\n        cleanupType,\n      },\n      cause,\n    );\n  }\n}\n\n/**\n * Thrown when the browser process fails to start or crashes immediately\n * Usually retryable as it may be a temporary issue\n */\nexport class BrowserProcessError extends BaseLaunchError {\n  constructor(\n    message: string,\n    processState: BrowserProcessState,\n    cause?: unknown,\n    exitCode?: number,\n  ) {\n    super(\n      LaunchErrorType.BROWSER_PROCESS,\n      `Browser process error (${processState}): ${message}`,\n      true,\n      {\n        processState,\n        exitCode,\n      },\n      cause,\n    );\n  }\n}\n\n/**\n * Thrown when session context injection fails\n * Usually retryable, can fall back to launching without context\n */\nexport class SessionContextError extends BaseLaunchError {\n  constructor(message: string, contextType: SessionContextType, cause?: unknown) {\n    super(\n      LaunchErrorType.SESSION_CONTEXT,\n      `Session context error with ${contextType}: ${message}`,\n      true,\n      {\n        contextType,\n      },\n      cause,\n    );\n  }\n}\n\n/**\n * Utility function to categorize unknown errors\n */\nexport function categorizeError(error: unknown, context?: string): BaseLaunchError {\n  if (error instanceof BaseLaunchError) {\n    return error;\n  }\n\n  const errorMessage = error instanceof Error ? error.message : String(error);\n  const errorStack = error instanceof Error ? error.stack : undefined;\n\n  // Analyze error message patterns to categorize\n  const lowerMessage = errorMessage.toLowerCase();\n\n  if (lowerMessage.includes(\"timeout\") || lowerMessage.includes(\"timed out\")) {\n    return new LaunchTimeoutError();\n  }\n\n  if (\n    lowerMessage.includes(\"enoent\") ||\n    lowerMessage.includes(\"not found\") ||\n    lowerMessage.includes(\"no such file\")\n  ) {\n    return new ResourceError(errorMessage, ResourceType.FILE, false);\n  }\n\n  if (lowerMessage.includes(\"eacces\") || lowerMessage.includes(\"permission denied\")) {\n    return new SystemError(\n      errorMessage,\n      context ? SystemOperation.UNKNOWN_OPERATION : SystemOperation.FILE_ACCESS,\n    );\n  }\n\n  if (lowerMessage.includes(\"eaddrinuse\") || lowerMessage.includes(\"address already in use\")) {\n    return new NetworkError(errorMessage, NetworkOperation.PORT_BINDING);\n  }\n\n  if (lowerMessage.includes(\"proxy\") || lowerMessage.includes(\"websocket\")) {\n    return new NetworkError(\n      errorMessage,\n      context ? NetworkOperation.NETWORK_SETUP : NetworkOperation.NETWORK_SETUP,\n    );\n  }\n\n  if (lowerMessage.includes(\"fingerprint\")) {\n    return new FingerprintError(errorMessage, FingerprintStage.GENERATION);\n  }\n\n  if (lowerMessage.includes(\"plugin\")) {\n    return new PluginError(\n      errorMessage,\n      PluginName.UNKNOWN,\n      context ? PluginOperation.LAUNCH : PluginOperation.LAUNCH,\n    );\n  }\n\n  if (lowerMessage.includes(\"cleanup\") || lowerMessage.includes(\"clean\")) {\n    return new CleanupError(errorMessage, context ? CleanupType.GENERAL : CleanupType.GENERAL);\n  }\n\n  if (\n    lowerMessage.includes(\"chrome\") ||\n    lowerMessage.includes(\"browser\") ||\n    lowerMessage.includes(\"process\")\n  ) {\n    return new BrowserProcessError(errorMessage, BrowserProcessState.UNKNOWN);\n  }\n\n  // Default to system error for unrecognized errors\n  return new SystemError(\n    errorMessage,\n    context ? SystemOperation.UNKNOWN_OPERATION : SystemOperation.UNKNOWN_OPERATION,\n    error instanceof Error ? error : undefined,\n  );\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/browser-logger.test.ts",
    "content": "import { describe, it, expect, vi } from \"vitest\";\nimport { createBrowserLogger } from \"./browser-logger.js\";\nimport { BrowserEventType } from \"../../../types/enums.js\";\n\ndescribe(\"BrowserLogger\", () => {\n  it(\"should merge context into events\", () => {\n    const mockLog = vi.fn();\n    const baseLogger = {\n      info: mockLog,\n      warn: vi.fn(),\n      error: vi.fn(),\n      debug: vi.fn(),\n      fatal: vi.fn(),\n      trace: vi.fn(),\n      silent: vi.fn(),\n      child: vi.fn(),\n      level: \"info\",\n    };\n\n    const logger = createBrowserLogger({\n      baseLogger: baseLogger as any,\n      initialContext: { sessionId: \"test-session\", orgId: \"test-org\" },\n    });\n\n    logger.record({\n      type: BrowserEventType.Console,\n      timestamp: \"2025-01-01T00:00:00Z\",\n      console: { level: \"log\", text: \"test message\" },\n    });\n\n    expect(mockLog).toHaveBeenCalledWith(\n      {\n        sessionId: \"test-session\",\n        orgId: \"test-org\",\n        type: BrowserEventType.Console,\n        timestamp: \"2025-01-01T00:00:00Z\",\n        console: { level: \"log\", text: \"test message\" },\n      },\n      BrowserEventType.Console,\n    );\n  });\n\n  it(\"should allow dynamic context updates\", () => {\n    const mockLog = vi.fn();\n    const baseLogger = {\n      info: mockLog,\n      warn: vi.fn(),\n      error: vi.fn(),\n      debug: vi.fn(),\n      fatal: vi.fn(),\n      trace: vi.fn(),\n      silent: vi.fn(),\n      child: vi.fn(),\n      level: \"info\",\n    };\n\n    const logger = createBrowserLogger({\n      baseLogger: baseLogger as any,\n      initialContext: { sessionId: \"session-1\" },\n    });\n\n    logger.setContext({ orgId: \"org-1\" });\n\n    expect(logger.getContext()).toEqual({\n      sessionId: \"session-1\",\n      orgId: \"org-1\",\n    });\n\n    logger.record({\n      type: BrowserEventType.Navigation,\n      timestamp: \"2025-01-01T00:00:00Z\",\n      navigation: { url: \"https://example.com\" },\n    });\n\n    expect(mockLog).toHaveBeenCalledWith(\n      expect.objectContaining({\n        sessionId: \"session-1\",\n        orgId: \"org-1\",\n        type: BrowserEventType.Navigation,\n      }),\n      BrowserEventType.Navigation,\n    );\n  });\n\n  it(\"should support functional context updates\", () => {\n    const mockLog = vi.fn();\n    const baseLogger = {\n      info: mockLog,\n      warn: vi.fn(),\n      error: vi.fn(),\n      debug: vi.fn(),\n      fatal: vi.fn(),\n      trace: vi.fn(),\n      silent: vi.fn(),\n      child: vi.fn(),\n      level: \"info\",\n    };\n\n    const logger = createBrowserLogger({\n      baseLogger: baseLogger as any,\n      initialContext: { count: 0 },\n    });\n\n    logger.setContext((prev) => ({ count: (prev.count as number) + 1 }));\n    expect(logger.getContext()).toEqual({ count: 1 });\n\n    logger.setContext((prev) => ({ count: (prev.count as number) + 1 }));\n    expect(logger.getContext()).toEqual({ count: 2 });\n  });\n\n  it(\"should prioritize event fields over context fields\", () => {\n    const mockLog = vi.fn();\n    const baseLogger = {\n      info: mockLog,\n      warn: vi.fn(),\n      error: vi.fn(),\n      debug: vi.fn(),\n      fatal: vi.fn(),\n      trace: vi.fn(),\n      silent: vi.fn(),\n      child: vi.fn(),\n      level: \"info\",\n    };\n\n    const logger = createBrowserLogger({\n      baseLogger: baseLogger as any,\n      initialContext: { type: \"wrong-type\", pageId: \"context-page\" },\n    });\n\n    logger.record({\n      type: BrowserEventType.Request,\n      timestamp: \"2025-01-01T00:00:00Z\",\n      pageId: \"event-page\",\n      request: { method: \"GET\", url: \"https://example.com\" },\n    });\n\n    expect(mockLog).toHaveBeenCalledWith(\n      expect.objectContaining({\n        type: BrowserEventType.Request,\n        pageId: \"event-page\",\n      }),\n      BrowserEventType.Request,\n    );\n  });\n});\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/browser-logger.ts",
    "content": "import { BrowserEventUnion } from \"./types.js\";\nimport { LogStorage } from \"./storage/index.js\";\nimport { EventEmitter } from \"events\";\nimport { BrowserEventType, EmitEvent } from \"../../../types/enums.js\";\n\nexport type Context = Record<string, any>;\n\n/**\n * Logger interface compatible with both pino.Logger and FastifyBaseLogger.\n * This avoids type conflicts between different pino versions (v9 vs v10).\n */\nexport interface Logger {\n  info(obj: object, msg?: string): void;\n  error(obj: object, msg?: string): void;\n}\n\nexport interface BrowserLogger {\n  record(event: BrowserEventUnion): void;\n  resetContext(): void;\n  setContext(\n    update: Partial<Context> | ((prev: Readonly<Context>) => Partial<Context> | Context),\n  ): void;\n  getContext(): Readonly<Context>;\n  flush?(): Promise<void>;\n  getStorage?(): LogStorage | null;\n  on?(event: EmitEvent.Log, listener: (event: BrowserEventUnion, context: Context) => void): this;\n  off?(event: EmitEvent.Log, listener: (event: BrowserEventUnion, context: Context) => void): this;\n}\n\nexport interface CreateBrowserLoggerOptions {\n  baseLogger: Logger;\n  initialContext?: Context;\n  storage?: LogStorage;\n  enableConsoleLogging?: boolean;\n}\n\nexport function createBrowserLogger(options: CreateBrowserLoggerOptions): BrowserLogger {\n  let context: Context = options.initialContext ?? {};\n  const storage = options.storage || null;\n  const enableConsoleLogging = options.enableConsoleLogging ?? true;\n  const eventEmitter = new EventEmitter();\n\n  const resetContext = () => {\n    context = options.initialContext ?? {};\n  };\n\n  const setContext = (\n    update: Partial<Context> | ((prev: Readonly<Context>) => Partial<Context> | Context),\n  ) => {\n    if (typeof update === \"function\") {\n      const result = update(context);\n      context = { ...context, ...result };\n    } else {\n      context = { ...context, ...update };\n    }\n  };\n\n  const getContext = (): Readonly<Context> => context;\n\n  const record = (event: BrowserEventUnion) => {\n    const mergedEvent = { ...context, ...event };\n\n    if (enableConsoleLogging) {\n      options.baseLogger.info(mergedEvent, event.type);\n    }\n\n    if (storage) {\n      storage.write(event, context).catch((err) => {\n        options.baseLogger.error({ err }, \"Failed to write event to storage\");\n      });\n    }\n\n    if (event.type !== BrowserEventType.Recording) eventEmitter.emit(EmitEvent.Log, event, context);\n  };\n\n  const flush = async () => {\n    if (storage) {\n      await storage.flush();\n    }\n  };\n\n  const getStorage = () => storage;\n\n  const on = (\n    event: EmitEvent.Log,\n    listener: (event: BrowserEventUnion, context: Context) => void,\n  ) => {\n    eventEmitter.on(event, listener);\n    return logger;\n  };\n\n  const off = (\n    event: EmitEvent.Log,\n    listener: (event: BrowserEventUnion, context: Context) => void,\n  ) => {\n    eventEmitter.off(event, listener);\n    return logger;\n  };\n\n  const logger: BrowserLogger = {\n    record,\n    resetContext,\n    setContext,\n    getContext,\n    flush,\n    getStorage,\n    on,\n    off,\n  };\n\n  return logger;\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/cdp-events.ts",
    "content": "import type { CDPSession } from \"puppeteer-core\";\nimport { BrowserEventType } from \"../../../types/index.js\";\nimport { BrowserLogger } from \"./browser-logger.js\";\n\n/**\n * Attaches protocol tracing to a CDP session.\n * Logs only protocol commands / notifications so you can diff automation flow between runs.\n */\nexport function attachCDPEvents(session: CDPSession, logger: BrowserLogger): void {\n  const sessionId = session.id?.() ?? \"unknown\";\n  const ts = () => new Date().toISOString();\n  const originalSend = session.send.bind(session);\n  type Method = Parameters<typeof session.send>[0];\n\n  session.send = async function (method: Method, params?: object) {\n    const start = performance.now();\n\n    logger.record({\n      type: BrowserEventType.CDPCommand,\n      timestamp: ts(),\n      cdp: { command: method, params, sessionId },\n    });\n\n    try {\n      const result = await originalSend(method, params);\n      logger.record({\n        type: BrowserEventType.CDPCommandResult,\n        timestamp: ts(),\n        cdp: {\n          command: method,\n          duration: performance.now() - start,\n          sessionId,\n          success: true,\n        },\n      });\n      return result;\n    } catch (err) {\n      logger.record({\n        type: BrowserEventType.CDPCommandResult,\n        timestamp: ts(),\n        cdp: {\n          command: method,\n          duration: performance.now() - start,\n          sessionId,\n          success: false,\n          error: (err as Error).message,\n        },\n      });\n      throw err;\n    }\n  } as typeof session.send;\n\n  const ignore = new Set([\n    \"Runtime.consoleAPICalled\",\n    \"Log.entryAdded\",\n    // Network events are handled by page-events.ts via typed Request/Response events.\n    // Suppress here to avoid duplicate logging.\n    \"Network.requestWillBeSent\",\n    \"Network.responseReceived\",\n    \"Network.dataReceived\",\n    \"Network.loadingFinished\",\n    \"Network.loadingFailed\",\n    \"Network.requestServedFromCache\",\n    \"Network.requestWillBeSentExtraInfo\",\n    \"Network.responseReceivedExtraInfo\",\n  ]);\n  session.on(\"event\", (event: any) => {\n    const { method, params } = event;\n    if (ignore.has(method)) return;\n    logger.record({\n      type: BrowserEventType.CDPEvent,\n      timestamp: ts(),\n      cdp: { name: method, params },\n    });\n  });\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/extension-events.ts",
    "content": "import { Protocol, Target, TargetType } from \"puppeteer-core\";\nimport type { BrowserLogger } from \"./browser-logger.js\";\nimport { ExtensionEvent } from \"./types.js\";\nimport { BrowserEventType } from \"../../../types/enums.js\";\nimport { formatLocation, serializeRemoteObject } from \"./utils.js\";\nimport type { FastifyBaseLogger } from \"fastify\";\n\nexport async function attachExtensionEvents(\n  target: Target,\n  logger: BrowserLogger,\n  internalExtensions: Set<string>,\n  appLogger: FastifyBaseLogger,\n): Promise<void> {\n  const url = target.url();\n  if (!url.startsWith(\"chrome-extension://\")) return;\n\n  const extensionId = url.split(\"/\")[2];\n  const isInternal = internalExtensions.has(extensionId);\n  const serviceWorkerId = (target as any)._targetId as string;\n  const targetType = target.type() as TargetType;\n\n  const session = await target.createCDPSession();\n\n  const emitExtensionEvent = (\n    partial: Pick<ExtensionEvent, \"logLevel\" | \"message\" | \"type\" | \"loc\" | \"executionContextId\">,\n  ) => {\n    const event: ExtensionEvent = {\n      type: partial.type,\n      logLevel: partial.logLevel,\n      message: partial.message,\n      extensionId,\n      serviceWorkerId,\n      timestamp: new Date().toISOString(),\n      targetType,\n      loc: partial.loc,\n      executionContextId: partial.executionContextId,\n    };\n\n    if (isInternal) {\n      const prefix = `[INTERNAL EXT ${extensionId}] ${event.type}`;\n      const locSuffix = event.loc ? ` (${event.loc})` : \"\";\n      appLogger.info(`${prefix} (${event.logLevel}) ${event.message + locSuffix}`);\n      return;\n    }\n\n    logger.record(event);\n  };\n\n  session.on(\"Runtime.consoleAPICalled\", (ev: Protocol.Runtime.ConsoleAPICalledEvent) => {\n    const text = ev.args.map(serializeRemoteObject).join(\" \");\n    const loc = formatLocation(ev.stackTrace);\n\n    emitExtensionEvent({\n      type: BrowserEventType.Console,\n      logLevel: ev.type === \"error\" ? \"error\" : ev.type === \"warning\" ? \"warn\" : \"log\",\n      message: text,\n      loc,\n      executionContextId: ev.executionContextId,\n    });\n  });\n\n  session.on(\"Runtime.exceptionThrown\", (ev: Protocol.Runtime.ExceptionThrownEvent) => {\n    const desc = ev.exceptionDetails.exception?.description ?? ev.exceptionDetails.text;\n    emitExtensionEvent({\n      type: BrowserEventType.PageError,\n      logLevel: \"error\",\n      message: desc,\n      loc: ev.exceptionDetails.url\n        ? `${ev.exceptionDetails.url}:${ev.exceptionDetails.lineNumber}:${ev.exceptionDetails.columnNumber}`\n        : undefined,\n      executionContextId: ev.exceptionDetails.executionContextId,\n    });\n  });\n\n  session.on(\"Network.loadingFailed\", (ev: Protocol.Network.LoadingFailedEvent) => {\n    emitExtensionEvent({\n      type: BrowserEventType.RequestFailed,\n      logLevel: \"error\",\n      message: ev.errorText,\n    });\n  });\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/page-console.ts",
    "content": ""
  },
  {
    "path": "api/src/services/cdp/instrumentation/page-events.ts",
    "content": "import type { Page, CDPSession, TargetType, Protocol } from \"puppeteer-core\";\nimport { BrowserEventType } from \"../../../types/index.js\";\nimport { BrowserLogger } from \"./browser-logger.js\";\nimport { formatLocation, serializeRemoteObject } from \"./utils.js\";\n\nexport interface AttachPageEventsOptions {\n  dangerouslyLogRequestDetails?: boolean;\n}\n\nconst MAX_BODY_SIZE = 1_048_576; // 1 MB\nconst TEXT_MIME_PREFIXES = [\"text/\", \"application/json\", \"application/xml\", \"application/xhtml\"];\n\nfunction isTextMime(mime: string | undefined): boolean {\n  if (!mime) return false;\n  const lower = mime.toLowerCase();\n  return TEXT_MIME_PREFIXES.some((p) => lower.startsWith(p));\n}\n\n/**\n * Attach page-level event listeners. The caller must pass an already-enabled\n * CDP session (with Network, Runtime, Log domains enabled) so that all\n * listeners share a single session per target.\n */\nexport async function attachPageEvents(\n  page: Page,\n  session: CDPSession,\n  logger: BrowserLogger,\n  targetType: TargetType,\n  options?: AttachPageEventsOptions,\n): Promise<void> {\n  const pageId = (page.target() as any)._targetId as string;\n  const logBodies = options?.dangerouslyLogRequestDetails === true;\n\n  // navigation\n  page.on(\"framenavigated\", (frame) => {\n    if (frame.parentFrame()) return;\n    logger.record({\n      type: BrowserEventType.Navigation,\n      timestamp: new Date().toISOString(),\n      pageId,\n      targetType,\n      navigation: { url: frame.url() },\n    });\n  });\n\n  // initial page\n  logger.record({\n    type: BrowserEventType.Navigation,\n    timestamp: new Date().toISOString(),\n    pageId,\n    targetType,\n    navigation: { url: page.url() },\n  });\n\n  // Track request metadata by requestId for use in loadingFailed (url) and loadingFinished (mimeType)\n  const requestMeta = new Map<string, { url: string; mimeType?: string }>();\n\n  // Network request logging via CDP Network domain.\n  // This fires for ALL requests including form POST navigations, unlike\n  // Puppeteer's page.on(\"request\") which depends on Fetch interception\n  // and can miss requests during same-tab navigations.\n  session.on(\"Network.requestWillBeSent\", (event: Protocol.Network.RequestWillBeSentEvent) => {\n    requestMeta.set(event.requestId, { url: event.request.url });\n\n    logger.record({\n      type: BrowserEventType.Request,\n      timestamp: new Date().toISOString(),\n      pageId,\n      targetType,\n      request: {\n        method: event.request.method,\n        url: event.request.url,\n        resourceType: event.type,\n        ...(logBodies && event.request.postData ? { postData: event.request.postData } : {}),\n        ...(logBodies && event.request.headers\n          ? { headers: event.request.headers as Record<string, string> }\n          : {}),\n      },\n    });\n  });\n\n  session.on(\"Network.responseReceived\", (event: Protocol.Network.ResponseReceivedEvent) => {\n    const meta = requestMeta.get(event.requestId);\n    if (meta) {\n      meta.mimeType = event.response.mimeType;\n    }\n\n    const responseData: {\n      status: number;\n      url: string;\n      mimeType?: string;\n      headers?: Record<string, string>;\n    } = {\n      status: event.response.status,\n      url: event.response.url,\n      mimeType: event.response.mimeType,\n    };\n\n    if (logBodies && event.response.headers) {\n      responseData.headers = event.response.headers as Record<string, string>;\n    }\n\n    logger.record({\n      type: BrowserEventType.Response,\n      timestamp: new Date().toISOString(),\n      pageId,\n      targetType,\n      response: responseData,\n    });\n  });\n\n  // Always listen for loadingFinished to clean up requestMeta entries.\n  // When dangerouslyLogRequestDetails is enabled, also capture response bodies\n  // (size-capped, text-only MIME types).\n  session.on(\"Network.loadingFinished\", (event: Protocol.Network.LoadingFinishedEvent) => {\n    const meta = requestMeta.get(event.requestId);\n    requestMeta.delete(event.requestId);\n\n    if (!logBodies) return;\n    if (event.encodedDataLength > MAX_BODY_SIZE) return;\n    if (!isTextMime(meta?.mimeType)) return;\n\n    session\n      .send(\"Network.getResponseBody\", { requestId: event.requestId })\n      .then((result) => {\n        if (result?.body) {\n          logger.record({\n            type: BrowserEventType.ResponseBody,\n            timestamp: new Date().toISOString(),\n            pageId,\n            targetType,\n            responseBody: {\n              requestId: event.requestId,\n              body: result.body,\n              base64Encoded: result.base64Encoded,\n            },\n          });\n        }\n      })\n      .catch(() => {\n        // Response body not available (redirects, evicted, etc.) — ignore\n      });\n  });\n\n  session.on(\"Network.loadingFailed\", (event: Protocol.Network.LoadingFailedEvent) => {\n    const url = requestMeta.get(event.requestId)?.url;\n    requestMeta.delete(event.requestId);\n\n    logger.record({\n      type: BrowserEventType.RequestFailed,\n      timestamp: new Date().toISOString(),\n      pageId,\n      targetType,\n      error: { message: event.errorText, url },\n    });\n  });\n\n  session.on(\"Runtime.consoleAPICalled\", (event: Protocol.Runtime.ConsoleAPICalledEvent) => {\n    const text = event.args.map(serializeRemoteObject).join(\" \");\n    const loc = formatLocation(event.stackTrace);\n    const prefix = targetType === \"background_page\" ? \"[BG] \" : \"\";\n\n    logger.record({\n      type: BrowserEventType.Console,\n      timestamp: new Date().toISOString(),\n      pageId,\n      targetType,\n      console: { level: event.type, text: prefix + text, loc },\n    });\n  });\n\n  session.on(\"Runtime.exceptionThrown\", (event: Protocol.Runtime.ExceptionThrownEvent) => {\n    const desc = event.exceptionDetails.exception?.description ?? event.exceptionDetails.text;\n    logger.record({\n      type: BrowserEventType.PageError,\n      timestamp: new Date().toISOString(),\n      pageId,\n      targetType,\n      error: { message: desc },\n    });\n  });\n\n  page.on(\"error\", (err) => {\n    logger.record({\n      type: BrowserEventType.Error,\n      timestamp: new Date().toISOString(),\n      pageId,\n      targetType,\n      error: { message: err?.message, stack: err?.stack },\n    });\n  });\n\n  page.on(\"pageerror\", (err) => {\n    logger.record({\n      type: BrowserEventType.PageError,\n      timestamp: new Date().toISOString(),\n      pageId,\n      targetType,\n      error: { message: err?.message, stack: err?.stack },\n    });\n  });\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/storage/duckdb-storage.ts",
    "content": "import { Database } from \"duckdb-async\";\nimport path from \"path\";\nimport fs from \"fs/promises\";\nimport { LogStorage, LogQuery, LogQueryResult } from \"./log-storage.interface.js\";\nimport { BrowserEventUnion } from \"../types.js\";\nimport { randomUUID } from \"crypto\";\nimport { safeStringify } from \"./safe-json.js\";\n\nexport type ParquetCompression = \"zstd\" | \"snappy\" | \"gzip\" | \"none\";\n\nexport interface DuckDBStorageOptions {\n  /**\n   * Path to the database file. If not provided, uses in-memory database.\n   */\n  dbPath?: string;\n  /**\n   * Maximum number of threads DuckDB can use. Defaults to 2.\n   * Set to 1 for minimal CPU impact.\n   */\n  maxThreads?: number;\n  /**\n   * Memory limit for DuckDB (e.g., \"256MB\", \"1GB\"). Defaults to \"256MB\".\n   */\n  memoryLimit?: string;\n  /**\n   * Parquet compression algorithm. Defaults to \"snappy\" (fast, lower CPU).\n   * - \"snappy\": Fast compression, moderate size (recommended for CPU efficiency)\n   * - \"zstd\": Best compression ratio, higher CPU\n   * - \"gzip\": Good compression, moderate CPU\n   * - \"none\": No compression, fastest but largest files\n   */\n  parquetCompression?: ParquetCompression;\n  /**\n   * Enable automatic write buffering. When enabled, writes are batched\n   * and flushed periodically to reduce CPU spikes. Defaults to true.\n   */\n  enableWriteBuffer?: boolean;\n  /**\n   * Size of write buffer before auto-flush. Defaults to 100 events.\n   */\n  writeBufferSize?: number;\n  /**\n   * Interval in ms to flush write buffer. Defaults to 1000ms.\n   */\n  writeBufferFlushInterval?: number;\n}\n\nexport class DuckDBStorage implements LogStorage {\n  private db: Database | null = null;\n  private dbPath: string;\n  private maxThreads: number;\n  private memoryLimit: string;\n  private parquetCompression: ParquetCompression;\n  private isInitialized = false;\n\n  // Write buffer for batching\n  private writeBuffer: Array<{ event: BrowserEventUnion; context: Record<string, any> }> = [];\n  private writeBufferEnabled: boolean;\n  private writeBufferSize: number;\n  private writeBufferFlushInterval: number;\n  private flushTimer: NodeJS.Timeout | null = null;\n  private isFlushing = false;\n\n  constructor(options: DuckDBStorageOptions = {}) {\n    this.dbPath = options.dbPath || \":memory:\";\n    this.maxThreads = options.maxThreads ?? 2;\n    this.memoryLimit = options.memoryLimit ?? \"256MB\";\n    this.parquetCompression = options.parquetCompression ?? \"snappy\";\n    this.writeBufferEnabled = options.enableWriteBuffer ?? true;\n    this.writeBufferSize = options.writeBufferSize ?? 100;\n    this.writeBufferFlushInterval = options.writeBufferFlushInterval ?? 1000;\n  }\n\n  async initialize(): Promise<void> {\n    if (this.isInitialized) return;\n\n    this.db = await Database.create(this.dbPath, {});\n\n    // Throttle CPU usage: limit threads and memory\n    await this.db.run(`SET threads = ${this.maxThreads}`);\n    await this.db.run(`SET memory_limit = '${this.memoryLimit}'`);\n\n    console.log(`DuckDB initialized: threads=${this.maxThreads}, memory=${this.memoryLimit}`);\n\n    await this.db.run(`\n      CREATE TABLE IF NOT EXISTS browser_events (\n        id VARCHAR PRIMARY KEY,\n        timestamp TIMESTAMP NOT NULL,\n        event_type VARCHAR NOT NULL,\n        target_type VARCHAR,\n        page_id VARCHAR,\n        data JSON NOT NULL,\n        context JSON NOT NULL,\n        indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n      )\n    `);\n\n    console.log(\"Created browser_events table\");\n\n    await this.db.run(`\n      CREATE INDEX IF NOT EXISTS idx_timestamp ON browser_events(timestamp);\n    `);\n    await this.db.run(`\n      CREATE INDEX IF NOT EXISTS idx_event_type ON browser_events(event_type);\n    `);\n    await this.db.run(`\n      CREATE INDEX IF NOT EXISTS idx_page_id ON browser_events(page_id);\n    `);\n\n    // Start periodic flush timer if write buffering is enabled\n    if (this.writeBufferEnabled) {\n      this.startFlushTimer();\n    }\n\n    this.isInitialized = true;\n  }\n\n  private startFlushTimer(): void {\n    if (this.flushTimer) return;\n    this.flushTimer = setInterval(() => {\n      this.flushWriteBuffer().catch((err) => {\n        console.error(\"DuckDB flush error:\", err);\n      });\n    }, this.writeBufferFlushInterval);\n    // Don't block process exit\n    this.flushTimer.unref();\n  }\n\n  private stopFlushTimer(): void {\n    if (this.flushTimer) {\n      clearInterval(this.flushTimer);\n      this.flushTimer = null;\n    }\n  }\n\n  private async flushWriteBuffer(): Promise<void> {\n    if (this.isFlushing || this.writeBuffer.length === 0 || !this.db) return;\n\n    this.isFlushing = true;\n    const toFlush = this.writeBuffer.splice(0, this.writeBuffer.length);\n\n    try {\n      await this.writeBatchInternal(toFlush);\n    } catch (err) {\n      // Put events back on failure (at the front)\n      this.writeBuffer.unshift(...toFlush);\n      throw err;\n    } finally {\n      this.isFlushing = false;\n    }\n  }\n\n  async write(event: BrowserEventUnion, context: Record<string, any>): Promise<void> {\n    if (!this.db) {\n      throw new Error(\"Database not initialized\");\n    }\n\n    if (this.writeBufferEnabled) {\n      // Add to buffer\n      this.writeBuffer.push({ event, context });\n\n      // Flush if buffer is full\n      if (this.writeBuffer.length >= this.writeBufferSize) {\n        await this.flushWriteBuffer();\n      }\n      return;\n    }\n\n    // Direct write (no buffering)\n    await this.writeSingle(event, context);\n  }\n\n  private async writeSingle(event: BrowserEventUnion, context: Record<string, any>): Promise<void> {\n    if (!this.db) return;\n\n    const stmt = await this.db.prepare(`\n        INSERT INTO browser_events (id, timestamp, event_type, target_type, page_id, data, context, indexed_at)\n        VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)\n      `);\n\n    const id = randomUUID();\n    const timestamp = event.timestamp;\n    const eventType = event.type;\n    const targetType = event.targetType || null;\n    const pageId = event.pageId || null;\n    const data = safeStringify(event);\n    const contextJson = safeStringify(context);\n\n    await stmt.run(id, timestamp, eventType, targetType, pageId, data, contextJson);\n    await stmt.finalize();\n  }\n\n  async writeBatch(\n    events: Array<{ event: BrowserEventUnion; context: Record<string, any> }>,\n  ): Promise<void> {\n    if (!this.db) {\n      throw new Error(\"Database not initialized\");\n    }\n\n    if (events.length === 0) return;\n\n    if (this.writeBufferEnabled) {\n      // Add all to buffer\n      this.writeBuffer.push(...events);\n\n      // Flush if buffer exceeds threshold\n      if (this.writeBuffer.length >= this.writeBufferSize) {\n        await this.flushWriteBuffer();\n      }\n      return;\n    }\n\n    // Direct batch write (no buffering)\n    await this.writeBatchInternal(events);\n  }\n\n  private async writeBatchInternal(\n    events: Array<{ event: BrowserEventUnion; context: Record<string, any> }>,\n  ): Promise<void> {\n    if (!this.db || events.length === 0) return;\n\n    const values: string[] = [];\n    const params: any[] = [];\n\n    for (const { event, context } of events) {\n      values.push(\"(?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)\");\n\n      const id = randomUUID();\n      const timestamp = event.timestamp;\n      const eventType = event.type;\n      const targetType = event.targetType || null;\n      const pageId = event.pageId || null;\n      const data = safeStringify(event);\n      const contextJson = safeStringify(context);\n\n      params.push(id, timestamp, eventType, targetType, pageId, data, contextJson);\n    }\n\n    const sql = `\n      INSERT INTO browser_events (id, timestamp, event_type, target_type, page_id, data, context, indexed_at)\n      VALUES ${values.join(\", \")}\n    `;\n\n    await this.db.run(sql, ...params);\n  }\n\n  async flush(): Promise<void> {\n    // Flush any buffered writes\n    await this.flushWriteBuffer();\n  }\n\n  async query(query: LogQuery): Promise<LogQueryResult> {\n    if (!this.db) {\n      throw new Error(\"Database not initialized\");\n    }\n\n    const conditions: string[] = [];\n    const params: any[] = [];\n\n    if (query.startTime) {\n      conditions.push(\"timestamp >= ?\");\n      params.push(query.startTime.toISOString());\n    }\n\n    if (query.endTime) {\n      conditions.push(\"timestamp <= ?\");\n      params.push(query.endTime.toISOString());\n    }\n\n    if (query.eventTypes && query.eventTypes.length > 0) {\n      conditions.push(`event_type IN (${query.eventTypes.map(() => \"?\").join(\", \")})`);\n      params.push(...query.eventTypes);\n    }\n\n    if (query.pageId) {\n      conditions.push(\"page_id = ?\");\n      params.push(query.pageId);\n    }\n\n    if (query.targetType) {\n      conditions.push(\"target_type = ?\");\n      params.push(query.targetType);\n    }\n\n    const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n    const limit = query.limit || 100;\n    const offset = query.offset || 0;\n\n    const countQuery = `SELECT COUNT(*) as total FROM browser_events ${whereClause}`;\n    const countResult = await this.db.all(countQuery, ...params);\n    // COUNT(*) may come back as a BigInt from DuckDB bindings – coerce safely for JSON\n    const total = Number(countResult[0]?.total ?? 0);\n\n    const eventsQuery = `\n      SELECT data, context\n      FROM browser_events\n      ${whereClause}\n      ORDER BY timestamp DESC\n      LIMIT ? OFFSET ?\n    `;\n    const rows = await this.db.all(eventsQuery, ...params, limit, offset);\n\n    const events: BrowserEventUnion[] = rows.map((row: any) => {\n      const data = JSON.parse(row.data);\n      const context = JSON.parse(row.context);\n      return { ...data, ...context };\n    });\n\n    return {\n      events,\n      total,\n      hasMore: offset + limit < total,\n    };\n  }\n\n  supportsParquetExport(): boolean {\n    return true;\n  }\n\n  async exportToParquet(filePath: string, query?: LogQuery): Promise<string> {\n    if (!this.db) {\n      throw new Error(\"Database not initialized\");\n    }\n\n    // Build WHERE clause if query is provided\n    let whereClause = \"\";\n    const params: any[] = [];\n\n    if (query) {\n      const conditions: string[] = [];\n\n      if (query.startTime) {\n        conditions.push(\"timestamp >= ?\");\n        params.push(query.startTime.toISOString());\n      }\n\n      if (query.endTime) {\n        conditions.push(\"timestamp <= ?\");\n        params.push(query.endTime.toISOString());\n      }\n\n      if (query.eventTypes && query.eventTypes.length > 0) {\n        conditions.push(`event_type IN (${query.eventTypes.map(() => \"?\").join(\", \")})`);\n        params.push(...query.eventTypes);\n      }\n\n      if (query.pageId) {\n        conditions.push(\"page_id = ?\");\n        params.push(query.pageId);\n      }\n\n      if (query.targetType) {\n        conditions.push(\"target_type = ?\");\n        params.push(query.targetType);\n      }\n\n      whereClause = conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n    }\n\n    // Ensure output directory exists\n    const dir = path.dirname(filePath);\n    await fs.mkdir(dir, { recursive: true });\n\n    // Export to Parquet with configurable compression\n    const sanitizedPath = filePath.replace(/'/g, \"''\");\n    const compressionClause =\n      this.parquetCompression === \"none\"\n        ? \"\"\n        : `, COMPRESSION ${this.parquetCompression.toUpperCase()}`;\n    const exportQuery = `\n      COPY (\n        SELECT * FROM browser_events ${whereClause}\n      ) TO '${sanitizedPath}' (FORMAT PARQUET${compressionClause})\n    `;\n\n    await this.db.run(exportQuery);\n\n    return filePath;\n  }\n\n  async getStats(): Promise<{\n    totalEvents: number;\n    oldestEvent: Date | null;\n    newestEvent: Date | null;\n    sizeBytes: number;\n  }> {\n    if (!this.db) {\n      throw new Error(\"Database not initialized\");\n    }\n\n    const result = await this.db.all(`\n      SELECT\n        COUNT(*) as total,\n        MIN(timestamp) as oldest,\n        MAX(timestamp) as newest\n      FROM browser_events\n    `);\n\n    const row = result[0];\n    let sizeBytes = 0;\n\n    // Get file size if using file-based storage\n    if (this.dbPath !== \":memory:\") {\n      const stats = await fs.stat(this.dbPath);\n      sizeBytes = stats.size;\n    }\n\n    return {\n      totalEvents: Number(row?.total ?? 0),\n      oldestEvent: row?.oldest ? new Date(row.oldest) : null,\n      newestEvent: row?.newest ? new Date(row.newest) : null,\n      sizeBytes,\n    };\n  }\n\n  async clear(options: { vacuum?: boolean } = {}): Promise<void> {\n    if (!this.db) {\n      throw new Error(\"Database not initialized\");\n    }\n\n    // Flush any pending writes first\n    await this.flushWriteBuffer();\n\n    await this.db.run(\"DELETE FROM browser_events\");\n\n    // VACUUM is CPU-intensive; run in background by default\n    if (options.vacuum) {\n      await this.db.run(\"VACUUM\");\n    } else {\n      // Fire-and-forget vacuum (don't block)\n      this.db.run(\"VACUUM\").catch((err) => {\n        console.error(\"Background VACUUM error:\", err);\n      });\n    }\n  }\n\n  /**\n   * Run VACUUM manually when CPU is idle. This reclaims disk space\n   * and optimizes the database, but is CPU-intensive.\n   */\n  async vacuum(): Promise<void> {\n    if (!this.db) {\n      throw new Error(\"Database not initialized\");\n    }\n    await this.db.run(\"VACUUM\");\n  }\n\n  async close(): Promise<void> {\n    // Stop the flush timer\n    this.stopFlushTimer();\n\n    // Flush any remaining buffered writes\n    if (this.writeBuffer.length > 0 && this.db) {\n      try {\n        await this.flushWriteBuffer();\n      } catch (err) {\n        console.error(\"Error flushing buffer on close:\", err);\n      }\n    }\n\n    if (this.db) {\n      await this.db.close();\n      this.db = null;\n    }\n\n    this.isInitialized = false;\n  }\n\n  /**\n   * Get current buffer stats for monitoring\n   */\n  getBufferStats(): { bufferedEvents: number; isBufferingEnabled: boolean } {\n    return {\n      bufferedEvents: this.writeBuffer.length,\n      isBufferingEnabled: this.writeBufferEnabled,\n    };\n  }\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/storage/in-memory-storage.ts",
    "content": "import { LogStorage, LogQuery, LogQueryResult } from \"./log-storage.interface.js\";\nimport { BrowserEventUnion } from \"../types.js\";\n\ninterface StoredEvent {\n  event: BrowserEventUnion;\n  context: Record<string, any>;\n  timestamp: Date;\n}\n\n/**\n * Simple in-memory log storage for testing or when persistence is not needed.\n * Note: This implementation does not support Parquet export.\n */\nexport class InMemoryStorage implements LogStorage {\n  private events: StoredEvent[] = [];\n  private maxEvents: number;\n\n  constructor(maxEvents: number = 10000) {\n    this.maxEvents = maxEvents;\n  }\n\n  async initialize(): Promise<void> {\n    // No initialization needed\n  }\n\n  async write(event: BrowserEventUnion, context: Record<string, any>): Promise<void> {\n    this.events.push({\n      event,\n      context,\n      timestamp: new Date(event.timestamp),\n    });\n\n    // Trim old events if we exceed the max\n    if (this.events.length > this.maxEvents) {\n      this.events = this.events.slice(-this.maxEvents);\n    }\n  }\n\n  async writeBatch(\n    events: Array<{ event: BrowserEventUnion; context: Record<string, any> }>,\n  ): Promise<void> {\n    for (const { event, context } of events) {\n      await this.write(event, context);\n    }\n  }\n\n  async query(query: LogQuery): Promise<LogQueryResult> {\n    let filtered = [...this.events];\n\n    // Apply filters\n    if (query.startTime) {\n      filtered = filtered.filter((e) => e.timestamp >= query.startTime!);\n    }\n\n    if (query.endTime) {\n      filtered = filtered.filter((e) => e.timestamp <= query.endTime!);\n    }\n\n    if (query.eventTypes && query.eventTypes.length > 0) {\n      filtered = filtered.filter((e) => query.eventTypes!.includes(e.event.type));\n    }\n\n    if (query.pageId) {\n      filtered = filtered.filter((e) => e.event.pageId === query.pageId);\n    }\n\n    if (query.targetType) {\n      filtered = filtered.filter((e) => e.event.targetType === query.targetType);\n    }\n\n    // Sort by timestamp descending\n    filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());\n\n    const total = filtered.length;\n    const limit = query.limit || 100;\n    const offset = query.offset || 0;\n\n    const events: BrowserEventUnion[] = filtered\n      .slice(offset, offset + limit)\n      .map((e) => ({ ...e.event, ...e.context }));\n\n    return {\n      events,\n      total,\n      hasMore: offset + limit < total,\n    };\n  }\n\n  supportsParquetExport(): boolean {\n    return false;\n  }\n\n  async exportToParquet(filePath: string, query?: LogQuery): Promise<string> {\n    throw new Error(\"Parquet export not supported in InMemoryStorage. Use DuckDBStorage instead.\");\n  }\n\n  async getStats(): Promise<{\n    totalEvents: number;\n    oldestEvent: Date | null;\n    newestEvent: Date | null;\n    sizeBytes: number;\n  }> {\n    const timestamps = this.events.map((e) => e.timestamp.getTime());\n\n    return {\n      totalEvents: this.events.length,\n      oldestEvent: timestamps.length > 0 ? new Date(Math.min(...timestamps)) : null,\n      newestEvent: timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null,\n      sizeBytes: JSON.stringify(this.events).length, // Approximate\n    };\n  }\n\n  async clear(): Promise<void> {\n    this.events = [];\n  }\n\n  async flush(): Promise<void> {\n    // No-op for in-memory storage\n  }\n\n  async close(): Promise<void> {\n    this.events = [];\n  }\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/storage/index.ts",
    "content": "export * from \"./log-storage.interface.js\";\nexport * from \"./duckdb-storage.js\";\nexport * from \"./in-memory-storage.js\";\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/storage/log-storage.interface.ts",
    "content": "import { BrowserEventUnion } from \"../types.js\";\n\nexport interface LogQuery {\n  startTime?: Date;\n  endTime?: Date;\n  eventTypes?: string[];\n  pageId?: string;\n  targetType?: string;\n  limit?: number;\n  offset?: number;\n}\n\nexport interface LogQueryResult {\n  events: BrowserEventUnion[];\n  total: number;\n  hasMore: boolean;\n}\n\nexport interface LogStorage {\n  /**\n   * Initialize the storage backend\n   */\n  initialize(): Promise<void>;\n\n  /**\n   * Write a single event to storage\n   */\n  write(event: BrowserEventUnion, context: Record<string, any>): Promise<void>;\n\n  /**\n   * Write multiple events in batch\n   */\n  writeBatch(\n    events: Array<{ event: BrowserEventUnion; context: Record<string, any> }>,\n  ): Promise<void>;\n\n  /**\n   * Query events from storage\n   */\n  query(query: LogQuery): Promise<LogQueryResult>;\n\n  /**\n   * Check if Parquet export is supported by this storage backend\n   */\n  supportsParquetExport(): boolean;\n\n  /**\n   * Export logs to Parquet format\n   */\n  exportToParquet(filePath: string, query?: LogQuery): Promise<string>;\n\n  /**\n   * Get statistics about stored logs\n   */\n  getStats(): Promise<{\n    totalEvents: number;\n    oldestEvent: Date | null;\n    newestEvent: Date | null;\n    sizeBytes: number;\n  }>;\n\n  /**\n   * Clear all logs\n   */\n  clear(): Promise<void>;\n\n  /**\n   * Flush any pending writes\n   */\n  flush(): Promise<void>;\n\n  /**\n   * Close the storage connection\n   */\n  close(): Promise<void>;\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/storage/safe-json.ts",
    "content": "export function safeStringify(value: unknown): string {\n  const seen = new WeakSet<object>();\n\n  return JSON.stringify(value, function (_key, currentValue) {\n    if (typeof currentValue === \"object\" && currentValue !== null) {\n      if (seen.has(currentValue)) {\n        return \"[Circular]\";\n      }\n\n      seen.add(currentValue);\n    }\n\n    return currentValue;\n  });\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/target-manager.ts",
    "content": "import { type Target, type CDPSession, TargetType } from \"puppeteer-core\";\nimport type { FastifyBaseLogger } from \"fastify\";\n\nimport { attachPageEvents, AttachPageEventsOptions } from \"./page-events.js\";\nimport { attachCDPEvents } from \"./cdp-events.js\";\nimport { attachExtensionEvents } from \"./extension-events.js\";\nimport { attachWorkerEvents } from \"./worker-events.js\";\nimport { BrowserLogger } from \"./browser-logger.js\";\n\nconst INTERNAL_EXTENSIONS = new Set<string>([\n  // TODO: need secret manager, recorder, and capacha IDs\n]);\n\nexport class TargetInstrumentationManager {\n  private attachedSessions = new Set<string>();\n  private cdpSessions = new Map<string, CDPSession>();\n\n  private pageEventsOptions: AttachPageEventsOptions;\n\n  constructor(\n    private logger: BrowserLogger,\n    private appLogger: FastifyBaseLogger,\n    pageEventsOptions?: AttachPageEventsOptions,\n  ) {\n    this.pageEventsOptions = pageEventsOptions ?? {};\n  }\n\n  async attach(target: Target, type: TargetType) {\n    const url = target.url?.() ?? \"\";\n    const isExtensionTarget = url.startsWith(\"chrome-extension://\");\n    const sessionId = (target as any)._targetId;\n\n    if (this.attachedSessions.has(sessionId)) {\n      return;\n    }\n\n    this.attachedSessions.add(sessionId);\n\n    switch (type) {\n      case TargetType.PAGE:\n      case TargetType.BACKGROUND_PAGE: {\n        // Create a single CDP session shared by page-events and cdp-events\n        const session = await target.createCDPSession();\n        this.cdpSessions.set(sessionId, session);\n        await this.enableDomainsForTarget(session, type, isExtensionTarget);\n\n        const page = await target.page();\n        if (page) {\n          await attachPageEvents(page, session, this.logger, type, this.pageEventsOptions);\n        }\n\n        attachCDPEvents(session, this.logger);\n\n        if (isExtensionTarget) {\n          await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger);\n        }\n        break;\n      }\n\n      case TargetType.SERVICE_WORKER: {\n        const session = await target.createCDPSession();\n        this.cdpSessions.set(sessionId, session);\n        await this.enableDomainsForTarget(session, type, isExtensionTarget);\n        attachCDPEvents(session, this.logger);\n\n        if (isExtensionTarget) {\n          await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger);\n        } else {\n          attachWorkerEvents(target, session, this.logger, type);\n        }\n        break;\n      }\n\n      case TargetType.SHARED_WORKER: {\n        const session = await target.createCDPSession();\n        this.cdpSessions.set(sessionId, session);\n        await this.enableDomainsForTarget(session, type, isExtensionTarget);\n        attachCDPEvents(session, this.logger);\n\n        if (isExtensionTarget) {\n          await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger);\n        } else {\n          attachWorkerEvents(target, session, this.logger, type);\n        }\n        break;\n      }\n\n      case TargetType.WEBVIEW: {\n        const session = await target.createCDPSession();\n        this.cdpSessions.set(sessionId, session);\n        await this.enableDomainsForTarget(session, type, isExtensionTarget);\n        attachCDPEvents(session, this.logger);\n\n        if (isExtensionTarget) {\n          await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger);\n        } else {\n          attachWorkerEvents(target, session, this.logger, type);\n        }\n        break;\n      }\n\n      case TargetType.BROWSER:\n      case TargetType.OTHER:\n      default: {\n        const session = await target.createCDPSession();\n        this.cdpSessions.set(sessionId, session);\n        await this.enableDomainsForTarget(session, type, isExtensionTarget);\n        attachCDPEvents(session, this.logger);\n\n        if (isExtensionTarget) {\n          await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger);\n        }\n        break;\n      }\n    }\n  }\n\n  detach(targetId: string) {\n    this.attachedSessions.delete(targetId);\n    const session = this.cdpSessions.get(targetId);\n    if (session) {\n      this.cdpSessions.delete(targetId);\n      session.detach().catch(() => {\n        // Session may already be closed if the target was destroyed\n      });\n    }\n  }\n\n  private async enableDomainsForTarget(\n    session: CDPSession,\n    type: TargetType,\n    isExtension: boolean,\n  ): Promise<void> {\n    const enabledDomains = new Set<string>();\n\n    const enable = async (domain: string) => {\n      if (enabledDomains.has(domain)) return;\n      try {\n        await session.send(`${domain}.enable` as any);\n        enabledDomains.add(domain);\n      } catch (err) {\n        this.appLogger.error({ err }, `[TargetManager] Failed to enable ${domain} for ${type}:`);\n      }\n    };\n\n    switch (type) {\n      case TargetType.PAGE:\n      case TargetType.BACKGROUND_PAGE:\n        await enable(\"Runtime\");\n        await enable(\"Log\");\n        await enable(\"Network\");\n        break;\n\n      case TargetType.SERVICE_WORKER:\n      case TargetType.SHARED_WORKER:\n        await enable(\"Runtime\");\n        await enable(\"Log\");\n        if (isExtension) {\n          await enable(\"Network\");\n        }\n        break;\n\n      case TargetType.WEBVIEW:\n      case TargetType.OTHER:\n        if (isExtension) {\n          await enable(\"Runtime\");\n          await enable(\"Log\");\n          await enable(\"Network\");\n        }\n        break;\n\n      default:\n        break;\n    }\n  }\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/types.ts",
    "content": "import type { TargetType } from \"puppeteer-core\";\nimport type { BrowserEventType } from \"../../../types/enums.js\";\n\nexport interface BaseBrowserEvent {\n  type: BrowserEventType;\n  timestamp: string;\n  targetType?: TargetType;\n  pageId?: string;\n}\n\nexport interface RequestEvent extends BaseBrowserEvent {\n  type: BrowserEventType.Request;\n  request: {\n    method: string;\n    url: string;\n    resourceType?: string;\n    postData?: string;\n    headers?: Record<string, string>;\n  };\n}\n\nexport interface ResponseEvent extends BaseBrowserEvent {\n  type: BrowserEventType.Response;\n  response: {\n    status: number;\n    url: string;\n    mimeType?: string;\n    headers?: Record<string, string>;\n    body?: string;\n  };\n}\n\nexport interface ResponseBodyEvent extends BaseBrowserEvent {\n  type: BrowserEventType.ResponseBody;\n  responseBody: {\n    requestId: string;\n    body: string;\n    base64Encoded: boolean;\n  };\n}\n\nexport interface NavigationEvent extends BaseBrowserEvent {\n  type: BrowserEventType.Navigation;\n  navigation: { url: string };\n}\n\nexport interface ConsoleEvent extends BaseBrowserEvent {\n  type: BrowserEventType.Console;\n  console: { level: string; text: string; loc?: string };\n}\n\nexport interface ErrorEvent extends BaseBrowserEvent {\n  type:\n    | BrowserEventType.PageError\n    | BrowserEventType.BrowserError\n    | BrowserEventType.Error\n    | BrowserEventType.RequestFailed;\n  error: { message: string; stack?: string; url?: string };\n}\n\nexport interface RecordingEvent extends BaseBrowserEvent {\n  type: BrowserEventType.Recording | BrowserEventType.ScreencastFrame;\n  data: any;\n}\n\nexport interface CDPEvent extends BaseBrowserEvent {\n  type: BrowserEventType.CDPEvent;\n  cdp: {\n    name: string;\n    params?: object;\n  };\n}\n\nexport interface CDPCommandEvent extends BaseBrowserEvent {\n  type: BrowserEventType.CDPCommand;\n  cdp: {\n    command: string;\n    params?: object;\n    sessionId: string;\n  };\n}\n\nexport interface CDPCommandResultEvent extends BaseBrowserEvent {\n  type: BrowserEventType.CDPCommandResult;\n  cdp: {\n    command: string;\n    duration: number;\n    sessionId: string;\n    success: boolean;\n    error?: string;\n  };\n}\n\nexport interface ExtensionEvent extends BaseBrowserEvent {\n  type: BrowserEventType.Console | BrowserEventType.PageError | BrowserEventType.RequestFailed;\n  extensionId: string;\n  serviceWorkerId?: string;\n  logLevel: \"log\" | \"warn\" | \"error\";\n  message: string;\n  loc?: string;\n  executionContextId?: number;\n}\n\nexport type BrowserEventUnion =\n  | RequestEvent\n  | ResponseEvent\n  | ResponseBodyEvent\n  | NavigationEvent\n  | ConsoleEvent\n  | ErrorEvent\n  | RecordingEvent\n  | CDPEvent\n  | CDPCommandEvent\n  | CDPCommandResultEvent\n  | ExtensionEvent;\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/utils.ts",
    "content": "import type { Target, Protocol } from \"puppeteer-core\";\nimport { safeStringify } from \"./storage/safe-json.js\";\n\nexport function extractTargetId(target: Target): string {\n  return (target as any)._targetId as string;\n}\n\nexport function serializeRemoteObject(obj: Protocol.Runtime.RemoteObject): string {\n  if (obj.value !== undefined) {\n    return typeof obj.value === \"object\" ? safeStringify(obj.value) : String(obj.value);\n  }\n  return obj.description ?? \"<unknown>\";\n}\n\nexport function formatLocation(stackTrace?: Protocol.Runtime.StackTrace): string | undefined {\n  if (!stackTrace?.callFrames?.[0]) return undefined;\n  const frame = stackTrace.callFrames[0];\n  return `${frame.url}:${frame.lineNumber + 1}:${frame.columnNumber + 1}`;\n}\n"
  },
  {
    "path": "api/src/services/cdp/instrumentation/worker-events.ts",
    "content": "import type { Target, Protocol, TargetType, CDPSession } from \"puppeteer-core\";\nimport { BrowserEventType } from \"../../../types/index.js\";\nimport { BrowserLogger } from \"./browser-logger.js\";\nimport { extractTargetId, formatLocation, serializeRemoteObject } from \"./utils.js\";\n\nexport function attachWorkerEvents(\n  target: Target,\n  session: CDPSession,\n  logger: BrowserLogger,\n  targetType: TargetType,\n): void {\n  const targetId = extractTargetId(target);\n\n  session.on(\"Runtime.consoleAPICalled\", (event: Protocol.Runtime.ConsoleAPICalledEvent) => {\n    const text = event.args.map(serializeRemoteObject).join(\" \");\n    const loc = formatLocation(event.stackTrace);\n\n    logger.record({\n      type: BrowserEventType.Console,\n      timestamp: new Date().toISOString(),\n      pageId: targetId,\n      targetType,\n      console: { level: event.type, text, loc },\n    });\n  });\n\n  session.on(\"Runtime.exceptionThrown\", (event: Protocol.Runtime.ExceptionThrownEvent) => {\n    const desc = event.exceptionDetails.exception?.description ?? event.exceptionDetails.text;\n    logger.record({\n      type: BrowserEventType.PageError,\n      timestamp: new Date().toISOString(),\n      pageId: targetId,\n      targetType,\n      error: { message: desc },\n    });\n  });\n}\n"
  },
  {
    "path": "api/src/services/cdp/plugins/core/base-plugin.ts",
    "content": "import type { Browser, Page } from \"puppeteer-core\";\nimport type { CDPService } from \"../../cdp.service.js\";\nimport type { BrowserLauncherOptions } from \"../../../../types/browser.js\";\n\nexport interface PluginOptions {\n  name: string;\n  [key: string]: any;\n}\n\nexport abstract class BasePlugin {\n  public name: string;\n  protected options: PluginOptions;\n  protected cdpService: CDPService | null;\n\n  constructor(options: PluginOptions) {\n    this.name = options.name;\n    this.options = options;\n    this.cdpService = null;\n  }\n\n  public setService(service: CDPService): void {\n    this.cdpService = service;\n  }\n\n  // Lifecycle methods\n  public async onBrowserLaunch(browser: Browser): Promise<void> {}\n  public onBrowserReady(context: BrowserLauncherOptions): void | Promise<void> {}\n  public async onPageCreated(page: Page): Promise<void> {}\n  public async onPageNavigate(page: Page): Promise<void> {}\n  public async onPageUnload(page: Page): Promise<void> {}\n  public async onBrowserClose(browser: Browser): Promise<void> {}\n  public async onBeforePageClose(page: Page): Promise<void> {}\n  public async onShutdown(): Promise<void> {}\n  public async onSessionEnd(sessionConfig: BrowserLauncherOptions): Promise<void> {}\n}\n\nexport type { BrowserLauncherOptions };\n"
  },
  {
    "path": "api/src/services/cdp/plugins/core/index.ts",
    "content": "export * from \"./base-plugin.js\";\nexport * from \"./plugin-manager.js\";\n"
  },
  {
    "path": "api/src/services/cdp/plugins/core/plugin-manager.ts",
    "content": "import { Browser, Page } from \"puppeteer-core\";\nimport { CDPService } from \"../../cdp.service.js\";\nimport { BasePlugin } from \"./base-plugin.js\";\nimport { FastifyBaseLogger } from \"fastify\";\nimport { BrowserLauncherOptions } from \"../../../../types/browser.js\";\n\nexport class PluginManager {\n  private plugins: Map<string, BasePlugin>;\n  private service: CDPService;\n  private logger: FastifyBaseLogger;\n\n  constructor(service: CDPService, logger: FastifyBaseLogger) {\n    this.plugins = new Map();\n    this.service = service;\n    this.logger = logger;\n  }\n\n  /**\n   * Register a plugin with the plugin manager\n   */\n  public register(plugin: BasePlugin): void {\n    if (this.plugins.has(plugin.name)) {\n      this.logger.warn(`Plugin with name ${plugin.name} is already registered. Overwriting.`);\n    }\n\n    plugin.setService(this.service);\n    this.plugins.set(plugin.name, plugin);\n    this.logger.info(`Registered plugin: ${plugin.name}`);\n  }\n\n  /**\n   * Unregister a plugin from the plugin manager\n   */\n  public unregister(pluginName: string): boolean {\n    const result = this.plugins.delete(pluginName);\n    if (result) {\n      this.logger.info(`Unregistered plugin: ${pluginName}`);\n    } else {\n      this.logger.warn(`Plugin with name ${pluginName} was not registered`);\n    }\n    return result;\n  }\n\n  /**\n   * Get a plugin by name\n   */\n  public getPlugin<T extends BasePlugin>(pluginName: string): T | undefined {\n    return this.plugins.get(pluginName) as T | undefined;\n  }\n\n  /**\n   * Notify all plugins about a browser launch\n   */\n  public async onBrowserLaunch(browser: Browser): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        await plugin.onBrowserLaunch(browser);\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onBrowserLaunch: ${error}`);\n      }\n    });\n    await Promise.all(promises);\n  }\n\n  public async onBrowserReady(context: BrowserLauncherOptions): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        // handle both async and sync hooks\n        await Promise.resolve(plugin.onBrowserReady(context));\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onBrowserReady: ${error}`);\n      }\n    });\n\n    await Promise.all(promises);\n  }\n\n  /**\n   * Notify all plugins about a page creation\n   */\n  public async onPageCreated(page: Page): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        await plugin.onPageCreated(page);\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onPageCreated: ${error}`);\n      }\n    });\n    await Promise.all(promises);\n  }\n\n  /**\n   * Notify all plugins before browser closes\n   */\n  public async onBrowserClose(browser: Browser): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        await plugin.onBrowserClose(browser);\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onBrowserClose: ${error}`);\n      }\n    });\n    await Promise.all(promises);\n  }\n\n  /**\n   * Notify all plugins before a page navigates\n   */\n  public async onPageNavigate(page: Page): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        await plugin.onPageNavigate(page);\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onPageNavigate: ${error}`);\n      }\n    });\n    await Promise.all(promises);\n  }\n\n  /**\n   * Notify all plugins before a page unloads\n   */\n  public async onPageUnload(page: Page): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        await plugin.onPageUnload(page);\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onPageUnload: ${error}`);\n      }\n    });\n    await Promise.all(promises);\n  }\n\n  /**\n   * Notify all plugins before a page closes\n   */\n  public async onBeforePageClose(page: Page): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        await plugin.onBeforePageClose(page);\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onBeforePageClose: ${error}`);\n      }\n    });\n    await Promise.all(promises);\n  }\n\n  /**\n   * Notify all plugins about shutdown\n   */\n  public async onShutdown(): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        await plugin.onShutdown();\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onShutdown: ${error}`);\n      }\n    });\n    await Promise.all(promises);\n  }\n\n  /**\n   * Notify all plugins when a session has ended\n   */\n  public async onSessionEnd(sessionConfig: BrowserLauncherOptions): Promise<void> {\n    const promises = Array.from(this.plugins.values()).map(async (plugin) => {\n      try {\n        await plugin.onSessionEnd(sessionConfig);\n      } catch (error) {\n        this.logger.error(`Error in plugin ${plugin.name}.onSessionEnd: ${error}`);\n      }\n    });\n    await Promise.all(promises);\n  }\n}\n"
  },
  {
    "path": "api/src/services/cdp/plugins/pptr-extensions.d.ts",
    "content": "import { SessionManager } from \"./session/session-manager.js\";\n\ndeclare module \"puppeteer-core\" {\n  interface Page {\n    session: SessionManager;\n  }\n}\n"
  },
  {
    "path": "api/src/services/cdp/utils/error-handlers.ts",
    "content": "import { FastifyBaseLogger } from \"fastify\";\nimport { BaseLaunchError } from \"../errors/launch-errors.js\";\n\n/**\n * Executes a critical operation that must succeed. Throws a categorized error on failure.\n *\n * @param operation - The async operation to execute\n * @param errorFactory - Factory function to create a categorized error from the caught error\n * @returns The result of the operation\n * @throws {BaseLaunchError} When the operation fails\n *\n * @example\n * const result = await executeCritical(\n *   async () => doSomethingCritical(),\n *   (error) => new BrowserProcessError(String(error), BrowserProcessState.LAUNCH_FAILED, error)\n * );\n */\nexport async function executeCritical<T>(\n  operation: () => Promise<T>,\n  errorFactory: (error: unknown) => BaseLaunchError,\n): Promise<T> {\n  try {\n    return await operation();\n  } catch (error) {\n    throw errorFactory(error);\n  }\n}\n\n/**\n * Executes a non-critical operation. Logs a warning on failure but continues execution.\n *\n * @param logger - Fastify logger instance for warning messages\n * @param operation - The async operation to execute\n * @param errorFactory - Factory function to create a categorized error from the caught error\n * @param defaultValue - Optional default value to return on failure\n * @returns The result of the operation, or defaultValue/undefined on failure\n *\n * @example\n * const result = await executeOptional(\n *   logger,\n *   async () => tryOptionalOperation(),\n *   (error) => new CleanupError(String(error), CleanupType.PRE_LAUNCH_FILE_CLEANUP),\n *   defaultValue\n * );\n */\nexport async function executeOptional<T>(\n  logger: FastifyBaseLogger,\n  operation: () => Promise<T>,\n  errorFactory: (error: unknown) => BaseLaunchError,\n  defaultValue?: T,\n): Promise<T | undefined> {\n  try {\n    return await operation();\n  } catch (error) {\n    const launchError = errorFactory(error);\n    logger.warn(`[CDPService] ${launchError.message} - continuing with launch`);\n    return defaultValue;\n  }\n}\n\n/**\n * Executes a best-effort operation. Silently logs on failure.\n *\n * @param logger - Fastify logger instance for debug messages\n * @param operation - The async operation to execute\n * @param logMessage - Message to log on failure\n * @returns The result of the operation, or undefined on failure\n *\n * @example\n * const result = await executeBestEffort(\n *   logger,\n *   async () => tryBestEffortOperation(),\n *   \"Failed to configure optional feature\"\n * );\n */\nexport async function executeBestEffort<T>(\n  logger: FastifyBaseLogger,\n  operation: () => Promise<T>,\n  logMessage: string,\n): Promise<T | undefined> {\n  try {\n    return await operation();\n  } catch (error) {\n    logger.debug(`[CDPService] ${logMessage}: ${error}`);\n    return undefined;\n  }\n}\n"
  },
  {
    "path": "api/src/services/cdp/utils/validation.ts",
    "content": "import { FastifyBaseLogger } from \"fastify\";\nimport { BrowserLauncherOptions } from \"../../../types/index.js\";\nimport { ConfigurationError, ConfigurationField } from \"../errors/launch-errors.js\";\n\n/**\n * Compares two Promise values by resolving them and checking if their serialized\n * representations are equal.\n * @param current - Current value or Promise<value>\n * @param next - Next value or Promise<value>\n * @returns Promise<boolean> - True if serialized values are equal\n */\nexport async function comparePromiseValues<T>(\n  current: T | Promise<T>,\n  next: T | Promise<T>,\n): Promise<boolean> {\n  try {\n    const [currentValue, nextValue] = await Promise.all([\n      Promise.resolve(current),\n      Promise.resolve(next),\n    ]);\n    return JSON.stringify(currentValue) === JSON.stringify(nextValue);\n  } catch (error) {\n    // If either promise rejects, consider them not equal\n    return false;\n  }\n}\n\n/**\n * Validates a given launch configuration (not conclusive)\n */\nexport function validateLaunchConfig(config: BrowserLauncherOptions): void {\n  // Validate dimensions\n  if (config.dimensions) {\n    if (config.dimensions.width <= 0 || config.dimensions.height <= 0) {\n      throw new ConfigurationError(\n        \"Dimensions must be positive numbers\",\n        ConfigurationField.DIMENSIONS,\n        config.dimensions,\n      );\n    }\n    if (config.dimensions.width > 7680 || config.dimensions.height > 4320) {\n      throw new ConfigurationError(\n        \"Dimensions are unreasonably large (max 7680x4320)\",\n        ConfigurationField.DIMENSIONS,\n        config.dimensions,\n      );\n    }\n  }\n\n  // Validates proxy URL format\n  if (config.options.proxyUrl) {\n    try {\n      new URL(config.options.proxyUrl);\n    } catch {\n      throw new ConfigurationError(\n        `Invalid proxy URL format: ${config.options.proxyUrl}`,\n        ConfigurationField.PROXY_URL,\n        config.options.proxyUrl,\n      );\n    }\n  }\n}\n\n/**\n * Validates and resolves the timezone configuration\n * @param logger - Fastify logger instance for warning messages\n * @param timezonePromise - Promise resolving to the timezone string\n * @param timeoutMs - Maximum time to wait for timezone resolution (default: 10000ms)\n * @returns Resolved and validated timezone string\n * @throws ConfigurationError if the timezone is invalid or cannot be resolved\n */\nexport async function validateTimezone(\n  logger: FastifyBaseLogger,\n  timezonePromise: Promise<string>,\n  timeoutMs: number = 10000,\n): Promise<string> {\n  try {\n    const timeoutPromise = new Promise<never>((_, reject) => {\n      setTimeout(() => {\n        reject(new Error(`Timezone validation timeout after ${timeoutMs}ms`));\n      }, timeoutMs);\n    });\n\n    const timezone = await Promise.race([timezonePromise, timeoutPromise]);\n    try {\n      Intl.DateTimeFormat(undefined, { timeZone: timezone });\n      return timezone;\n    } catch (timezoneError) {\n      throw new ConfigurationError(\n        `Invalid timezone resolved: ${timezone}`,\n        ConfigurationField.TIMEZONE,\n        timezone,\n      );\n    }\n  } catch (error) {\n    if (error instanceof ConfigurationError) {\n      throw error;\n    }\n    throw new ConfigurationError(\n      `Failed to resolve timezone: ${error}`,\n      ConfigurationField.TIMEZONE,\n      undefined,\n    );\n  }\n}\n\n/**\n * Checks if two launch configurations are reusable\n * @param current - The current launch configuration\n * @param next - The next launch configuration\n * @returns True if the configurations are reusable, false otherwise\n */\n\nexport async function isSimilarConfig(\n  current?: BrowserLauncherOptions,\n  next?: BrowserLauncherOptions,\n): Promise<boolean> {\n  if (!current || !next) {\n    return false;\n  }\n\n  // Start timezone comparison immediately (don't await yet)\n  // This allows the Promise to resolve in parallel with our synchronous checks\n  const timezoneComparisonPromise = comparePromiseValues(\n    current.timezone || Promise.resolve(\"\"),\n    next.timezone || Promise.resolve(\"\"),\n  );\n\n  const normalizeArgs = (args?: string[]) => (args || []).filter(Boolean).slice().sort();\n  const normalizeExt = (ext?: string[]) => (ext || []).slice().sort();\n\n  const currentHeadless = current.options?.headless ?? true;\n  const nextHeadless = next.options?.headless ?? true;\n\n  const currentProxy = current.options?.proxyUrl || \"\";\n  const nextProxy = next.options?.proxyUrl || \"\";\n\n  const currentArgs = normalizeArgs(current.options?.args);\n  const nextArgs = normalizeArgs(next.options?.args);\n\n  const currentExt = normalizeExt(current.extensions);\n  const nextExt = normalizeExt(next.extensions);\n\n  const currentBlockAds = current.blockAds ?? true;\n  const nextBlockAds = next.blockAds ?? true;\n\n  const currentUserAgent = current.userAgent || \"\";\n  const nextUserAgent = next.userAgent || \"\";\n\n  const currentUserDataDir = current.userDataDir || \"\";\n  const nextUserDataDir = next.userDataDir || \"\";\n\n  const currentSkipFingerprint = current.skipFingerprintInjection ?? false;\n  const nextSkipFingerprint = next.skipFingerprintInjection ?? false;\n\n  const currentWidth = current.dimensions?.width ?? 1920;\n  const nextWidth = next.dimensions?.width ?? 1920;\n\n  const currentHeight = current.dimensions?.height ?? 1080;\n  const nextHeight = next.dimensions?.height ?? 1080;\n\n  const {\n    session: _s1,\n    streaming: _w1,\n    ...currentExtra\n  } = (current.extra ?? {}) as Record<string, unknown>;\n  const {\n    session: _s2,\n    streaming: _w2,\n    ...nextExtra\n  } = (next.extra ?? {}) as Record<string, unknown>;\n\n  return (\n    currentHeadless === nextHeadless &&\n    currentProxy === nextProxy &&\n    currentUserAgent === nextUserAgent &&\n    currentUserDataDir === nextUserDataDir &&\n    currentSkipFingerprint === nextSkipFingerprint &&\n    currentWidth === nextWidth &&\n    currentHeight === nextHeight &&\n    currentBlockAds === nextBlockAds &&\n    JSON.stringify(currentArgs) === JSON.stringify(nextArgs) &&\n    JSON.stringify(currentExt) === JSON.stringify(nextExt) &&\n    JSON.stringify(currentExtra) === JSON.stringify(nextExtra) &&\n    JSON.stringify(current.userPreferences) === JSON.stringify(next.userPreferences) &&\n    JSON.stringify(current.deviceConfig) === JSON.stringify(next.deviceConfig) &&\n    (await timezoneComparisonPromise)\n  );\n}\n"
  },
  {
    "path": "api/src/services/context/chrome-context.service.ts",
    "content": "import { EventEmitter } from \"events\";\nimport { FastifyBaseLogger } from \"fastify\";\nimport { getProfilePath } from \"../../utils/context.js\";\nimport { ChromeLocalStorageReader } from \"../leveldb/localstorage.js\";\nimport { ChromeSessionStorageReader } from \"../leveldb/sessionstorage.js\";\nimport { SessionData } from \"./types.js\";\n\nexport class ChromeContextService extends EventEmitter {\n  private logger: FastifyBaseLogger;\n\n  constructor(logger: FastifyBaseLogger) {\n    super();\n    this.logger = logger;\n  }\n\n  /**\n   * Get all session data from a Chrome user data directory\n   * @param userDataDir Path to Chrome user data directory\n   * @returns SessionData containing cookies, localStorage, sessionStorage, and more\n   */\n  public async getSessionData(userDataDir?: string): Promise<SessionData> {\n    if (!userDataDir) {\n      this.logger.warn(\"No userDataDir specified, returning empty session data\");\n      return {\n        localStorage: {},\n        sessionStorage: {},\n        indexedDB: {},\n        cookies: [],\n      };\n    }\n\n    this.logger.info(`Extracting session data from Chrome user data directory: ${userDataDir}`);\n\n    try {\n      const sessionData: SessionData = {};\n\n      const [localStorage, sessionStorage] = await Promise.all([\n        this.extractLocalStorage(userDataDir),\n        this.extractSessionStorage(userDataDir),\n      ]);\n\n      if (localStorage && Object.keys(localStorage).length > 0) {\n        sessionData.localStorage = localStorage;\n      }\n\n      if (sessionStorage && Object.keys(sessionStorage).length > 0) {\n        sessionData.sessionStorage = sessionStorage;\n      }\n\n      return sessionData;\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      this.logger.error(`Error extracting session data: ${errorMessage}`);\n      throw new Error(`Failed to extract session data: ${errorMessage}`);\n    }\n  }\n\n  /**\n   * Extract localStorage from Chrome's LevelDB database\n   */\n  private async extractLocalStorage(\n    userDataDir: string,\n  ): Promise<Record<string, Record<string, string>>> {\n    const localStoragePath = getProfilePath(userDataDir, \"Local Storage\", \"leveldb\");\n    this.logger.info(`Extracting localStorage from ${localStoragePath}`);\n\n    try {\n      this.logger.info(`Reading localStorage from ${localStoragePath}`);\n      return await ChromeLocalStorageReader.readLocalStorage(localStoragePath);\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      this.logger.error(`Error extracting localStorage: ${errorMessage}`);\n      return {};\n    }\n  }\n\n  /**\n   * Extract sessionStorage from Chrome's Session Storage\n   */\n  private async extractSessionStorage(\n    userDataDir: string,\n  ): Promise<Record<string, Record<string, string>>> {\n    // Normalize path for cross-platform compatibility\n    const sessionStoragePath = getProfilePath(userDataDir, \"Session Storage\");\n\n    try {\n      this.logger.info(`Reading sessionStorage from ${sessionStoragePath}`);\n      const sessionStorage =\n        await ChromeSessionStorageReader.readSessionStorage(sessionStoragePath);\n      return sessionStorage;\n    } catch (error: unknown) {\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      this.logger.error(`Error extracting sessionStorage: ${errorMessage}`);\n      return {};\n    }\n  }\n}\n"
  },
  {
    "path": "api/src/services/context/types.ts",
    "content": "import { z } from \"zod\";\n\nexport enum StorageProviderName {\n  Cookies = \"cookies\",\n  LocalStorage = \"localStorage\",\n  SessionStorage = \"sessionStorage\",\n  IndexedDB = \"indexedDB\",\n}\n\nexport interface IndexedDBDatabaseWithOrigin {\n  id: number;\n  name: string;\n  origin: string;\n  objectStores: IndexedDBObjectStore[];\n}\n\nexport interface IndexedDBDatabase {\n  id: number;\n  name: string;\n  data: IndexedDBObjectStore[];\n}\n\nexport interface IndexedDBObjectStore {\n  id: number;\n  name: string;\n  records: IndexedDBRecord[];\n}\n\nexport interface IndexedDBRecord {\n  key: any;\n  value: any;\n  blobFiles?: IndexedDBBlobFile[];\n}\n\nexport interface IndexedDBBlobFile {\n  blobNumber: number;\n  mimeType: string;\n  size: number;\n  filename?: string;\n  lastModified?: Date;\n  path?: string;\n}\n\nexport type LocalStorageData = Record<string, string>;\nexport type SessionStorageData = Record<string, string>;\nexport type CookieData = z.infer<typeof CDPCookieSchema>;\n\nexport interface StorageProviderDataMap {\n  [StorageProviderName.LocalStorage]: LocalStorageData;\n  [StorageProviderName.SessionStorage]: SessionStorageData;\n  [StorageProviderName.Cookies]: CookieData[];\n  [StorageProviderName.IndexedDB]: Array<IndexedDBDatabase>;\n}\n\n// Utility type to get the data type for a specific provider\nexport type ProviderDataType<T extends StorageProviderName> = StorageProviderDataMap[T];\n\nexport type SessionData = {\n  [StorageProviderName.Cookies]?: CookieData[];\n  [StorageProviderName.LocalStorage]?: Record<string, LocalStorageData>;\n  [StorageProviderName.SessionStorage]?: Record<string, SessionStorageData>;\n  [StorageProviderName.IndexedDB]?: Record<string, Array<IndexedDBDatabase>>;\n};\n\n// Error classes\nexport class CorruptedSessionDataError extends Error {\n  constructor(zodError: z.ZodError) {\n    super(`Session data is corrupted: ${zodError.message}`);\n    this.name = \"CorruptedSessionDataError\";\n  }\n}\n\n// CDP related schemas\nexport const CDPSameSite = z.enum([\"Strict\", \"Lax\", \"None\"]);\nexport const CDPCookiePriority = z.enum([\"Low\", \"Medium\", \"High\"]);\nexport const CDPSourceScheme = z.enum([\"Unset\", \"NonSecure\", \"Secure\"]);\n\n/**\n * CDP Network.Cookie schema\n * @see https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie\n */\nexport const CDPCookieSchema = z.object({\n  name: z.string().describe(\"The name of the cookie\"),\n  value: z.string().describe(\"The value of the cookie\"),\n  url: z.string().optional().describe(\"The URL of the cookie\"),\n  domain: z.string().optional().describe(\"The domain of the cookie\"),\n  path: z.string().optional().describe(\"The path of the cookie\"),\n  secure: z.boolean().optional().describe(\"Whether the cookie is secure\"),\n  httpOnly: z.boolean().optional().describe(\"Whether the cookie is HTTP only\"),\n  sameSite: CDPSameSite.optional().describe(\"The same site attribute of the cookie\"),\n  size: z.number().optional().describe(\"The size of the cookie\"),\n  expires: z.number().optional().describe(\"The expiration date of the cookie\"),\n  partitionKey: z\n    .object({\n      topLevelSite: z\n        .string()\n        .describe(\n          \"The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie.\",\n        ),\n      hasCrossSiteAncestor: z\n        .boolean()\n        .describe(\n          \"Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.\",\n        ),\n    })\n    .optional()\n    .describe(\"The partition key of the cookie\"),\n  session: z.boolean().optional().describe(\"Whether the cookie is a session cookie\"),\n  priority: CDPCookiePriority.optional().describe(\"The priority of the cookie\"),\n  sameParty: z.boolean().optional().describe(\"Whether the cookie is a same party cookie\"),\n  sourceScheme: CDPSourceScheme.optional().describe(\"The source scheme of the cookie\"),\n  sourcePort: z.number().optional().describe(\"The source port of the cookie\"),\n});\nexport type CDPCookie = z.infer<typeof CDPCookieSchema>;\n\n// IndexedDB related schemas\nexport const IndexedDBBlobFileSchema = z.object({\n  blobNumber: z.number(),\n  mimeType: z.string(),\n  size: z.number(),\n  filename: z.string().optional(),\n  lastModified: z.date().optional(),\n  path: z.string().optional(),\n});\n\nexport const IndexedDBRecordSchema = z.object({\n  key: z.any(),\n  value: z.any(),\n  blobFiles: z.array(IndexedDBBlobFileSchema).optional(),\n});\n\nexport const IndexedDBObjectStoreSchema = z.object({\n  id: z.number(),\n  name: z.string(),\n  records: z.array(IndexedDBRecordSchema),\n});\n\nexport const IndexedDBDatabaseSchema = z.object({\n  id: z.number(),\n  name: z.string(),\n  data: z.array(IndexedDBObjectStoreSchema),\n});\n\n// Update the existing schema to use the new schemas\nexport const SessionContextSchema = z.object({\n  [StorageProviderName.Cookies]: z\n    .array(CDPCookieSchema)\n    .optional()\n    .describe(\"Cookies to initialize in the session\"),\n  [StorageProviderName.LocalStorage]: z\n    .record(z.string(), z.record(z.string(), z.string()))\n    .optional()\n    .describe(\"Domain-specific localStorage items to initialize in the session\"),\n  [StorageProviderName.SessionStorage]: z\n    .record(z.string(), z.record(z.string(), z.string()))\n    .optional()\n    .describe(\"Domain-specific sessionStorage items to initialize in the session\"),\n  [StorageProviderName.IndexedDB]: z\n    .record(z.string(), z.array(IndexedDBDatabaseSchema))\n    .optional()\n    .describe(\"Domain-specific indexedDB items to initialize in the session\"),\n});\n"
  },
  {
    "path": "api/src/services/file.service.ts",
    "content": "import archiver from \"archiver\";\nimport chokidar, { FSWatcher } from \"chokidar\";\nimport fs from \"fs\";\nimport type { DebouncedFunc } from \"lodash-es\";\nimport { debounce } from \"lodash-es\";\nimport { tmpdir } from \"os\";\nimport path, { resolve } from \"path\";\nimport { Readable } from \"stream\";\nimport { env } from \"../env.js\";\n\ninterface File {\n  size: number;\n  lastModified: Date;\n}\n\nexport class FileService {\n  private baseFilesPath: string;\n  private fileWatcher: FSWatcher | null = null;\n  private static instance: FileService | null = null;\n  private prebuiltArchiveDir: string;\n  private prebuiltArchivePath: string;\n  private isArchiving: boolean = false;\n  private archiveDebounceTime = 500;\n  private debouncedCreateArchive: DebouncedFunc<() => Promise<string | null>>;\n\n  private constructor() {\n    this.baseFilesPath = env.NODE_ENV === \"development\" ? path.join(tmpdir(), \"files\") : \"/files\";\n    this.prebuiltArchiveDir = \"/tmp/.steel\";\n    this.prebuiltArchivePath = path.join(this.prebuiltArchiveDir, \"files.zip\");\n\n    fs.mkdirSync(this.baseFilesPath, { recursive: true });\n\n    const boundCreateArchive = this._createArchive.bind(this);\n    this.debouncedCreateArchive = debounce(boundCreateArchive, this.archiveDebounceTime);\n\n    this.initFileWatcher();\n  }\n\n  public static getInstance() {\n    if (!FileService.instance) {\n      FileService.instance = new FileService();\n    }\n    return FileService.instance;\n  }\n\n  private async handleFileAdd(filePath: string) {\n    console.log(`[FileService] File added detected: ${filePath}`);\n    this.debouncedCreateArchive();\n  }\n\n  private handleFileDelete(filePath: string) {\n    console.log(`[FileService] File deleted detected: ${filePath}`);\n    this.debouncedCreateArchive();\n  }\n\n  private handleDirChange(filePath: string) {\n    console.log(`[FileService] Directory change detected: ${filePath}`);\n    this.debouncedCreateArchive();\n  }\n\n  private initFileWatcher() {\n    this.fileWatcher = chokidar.watch(this.baseFilesPath, {\n      ignored: /(^|[\\/\\\\])\\../,\n      persistent: true,\n      ignoreInitial: false,\n      awaitWriteFinish: {\n        stabilityThreshold: 500,\n        pollInterval: 100,\n      },\n      depth: undefined,\n    });\n\n    console.log(`[FileService] File watcher initialized for ${this.baseFilesPath}`);\n\n    this.fileWatcher\n      .on(\"add\", (filePath) => this.handleFileAdd(filePath))\n      .on(\"unlink\", (filePath) => this.handleFileDelete(filePath))\n      .on(\"addDir\", (filePath) => this.handleDirChange(filePath))\n      .on(\"unlinkDir\", (filePath) => this.handleDirChange(filePath))\n      .on(\"error\", (error) => console.error(`Watcher error: ${error}`))\n      .on(\"ready\", () => {\n        console.log(\"[FileService] Initial scan complete. Ready for changes.\");\n        this.debouncedCreateArchive();\n      });\n  }\n\n  private getSafeFilePath(relativePath: string) {\n    const resolvedPath = resolve(this.baseFilesPath, relativePath);\n    if (\n      !resolvedPath.startsWith(this.baseFilesPath + path.sep) &&\n      resolvedPath !== this.baseFilesPath\n    ) {\n      throw new Error(\"Invalid path\");\n    }\n    return resolvedPath;\n  }\n\n  private async exists(filePath: string): Promise<boolean> {\n    try {\n      await fs.promises.stat(filePath);\n\n      return true;\n    } catch (err: any) {\n      if (err.code === \"ENOENT\") return false;\n      throw err;\n    }\n  }\n\n  public async saveFile({\n    filePath,\n    stream,\n  }: {\n    filePath: string;\n    stream: Readable;\n  }): Promise<File & { path: string }> {\n    await fs.promises.mkdir(this.baseFilesPath, { recursive: true });\n\n    const safeFilePath = this.getSafeFilePath(filePath);\n    const parentDir = path.dirname(safeFilePath);\n    await fs.promises.mkdir(parentDir, { recursive: true });\n\n    try {\n      await fs.promises.writeFile(safeFilePath, stream);\n      const stats = await fs.promises.stat(safeFilePath);\n      const file: File = {\n        size: stats.size,\n        lastModified: stats.mtime,\n      };\n      console.log(`File saved: ${safeFilePath}, Size: ${file.size}`);\n      this.debouncedCreateArchive();\n      return { ...file, path: safeFilePath };\n    } catch (error) {\n      console.error(`[FileService] Error saving file ${safeFilePath}:`, error);\n\n      try {\n        if (await this.exists(safeFilePath)) {\n          await fs.promises.unlink(safeFilePath);\n        }\n      } catch (cleanupErr) {\n        console.error(\n          `[FileService] Failed to cleanup file ${safeFilePath} after save error:`,\n          cleanupErr,\n        );\n      }\n      throw error;\n    }\n  }\n\n  public async downloadFile({\n    filePath,\n  }: {\n    filePath: string;\n  }): Promise<{ stream: Readable } & File> {\n    await fs.promises.mkdir(this.baseFilesPath, { recursive: true });\n    const safeFilePath = this.getSafeFilePath(filePath);\n\n    try {\n      const stats = await fs.promises.stat(safeFilePath);\n      if (!stats.isFile()) {\n        throw new Error(`Requested path is not a file: ${safeFilePath}`);\n      }\n      const file: File = {\n        size: stats.size,\n        lastModified: stats.mtime,\n      };\n      const stream = fs.createReadStream(safeFilePath);\n      return {\n        stream,\n        ...file,\n      };\n    } catch (error: any) {\n      if (error.code === \"ENOENT\") {\n        throw new Error(`File not found: ${safeFilePath}`);\n      }\n      console.error(`[FileService] Error accessing file ${safeFilePath} for download:`, error);\n      throw new Error(`File not found or inaccessible: ${safeFilePath}`);\n    }\n  }\n\n  public async getFile({ filePath }: { filePath: string }): Promise<File> {\n    await fs.promises.mkdir(this.baseFilesPath, { recursive: true });\n    const safeFilePath = this.getSafeFilePath(filePath);\n\n    try {\n      const stats = await fs.promises.stat(safeFilePath);\n      if (!stats.isFile()) {\n        throw new Error(`Requested path is not a file: ${safeFilePath}`);\n      }\n      const file: File = {\n        size: stats.size,\n        lastModified: stats.mtime,\n      };\n      return file;\n    } catch (error: any) {\n      if (error.code === \"ENOENT\") {\n        throw new Error(`File not found: ${safeFilePath}`);\n      }\n      console.error(`[FileService] Error accessing file ${safeFilePath} for getFile:`, error);\n      throw new Error(`File not found or inaccessible: ${safeFilePath}`);\n    }\n  }\n\n  public async listFiles(): Promise<Array<{ path: string } & File>> {\n    await fs.promises.mkdir(this.baseFilesPath, { recursive: true });\n\n    const allFiles: Array<{ path: string } & File> = [];\n\n    const collectFilesRecursively = async (currentDir: string) => {\n      try {\n        const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });\n        for (const entry of entries) {\n          const entryPath = path.join(currentDir, entry.name);\n          if (entry.isFile()) {\n            try {\n              const stats = await fs.promises.stat(entryPath);\n              allFiles.push({\n                path: entryPath,\n                size: stats.size,\n                lastModified: stats.mtime,\n              });\n            } catch (statError) {\n              console.error(\n                `[FileService] Error getting stats for file ${entryPath} during listFiles:`,\n                statError,\n              );\n            }\n          } else if (entry.isDirectory()) {\n            await collectFilesRecursively(entryPath);\n          }\n        }\n      } catch (readDirError) {\n        console.error(\n          `[FileService] Error reading directory ${currentDir} during listFiles:`,\n          readDirError,\n        );\n      }\n    };\n\n    try {\n      await collectFilesRecursively(this.baseFilesPath);\n      allFiles.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());\n      return allFiles;\n    } catch (error) {\n      console.error(\n        `[FileService] Error listing files recursively from ${this.baseFilesPath}:`,\n        error,\n      );\n      return [];\n    }\n  }\n\n  public async deleteFile({ filePath }: { filePath: string }): Promise<void> {\n    await fs.promises.mkdir(this.baseFilesPath, { recursive: true });\n    const safeFilePath = this.getSafeFilePath(filePath);\n\n    if (!(await this.exists(safeFilePath))) {\n      console.log(\n        `[FileService] File ${safeFilePath} not found on disk during delete operation. Skipping.`,\n      );\n      return;\n    }\n\n    try {\n      const stats = await fs.promises.stat(safeFilePath);\n      if (!stats.isFile()) {\n        console.warn(`[FileService] Path ${safeFilePath} is not a file. Skipping delete.`);\n        return;\n      }\n      await fs.promises.unlink(safeFilePath);\n      console.log(`[FileService] File deleted: ${safeFilePath}`);\n      this.debouncedCreateArchive();\n    } catch (unlinkError) {\n      console.error(`Error unlinking file ${safeFilePath}:`, unlinkError);\n      throw unlinkError;\n    }\n\n    return;\n  }\n\n  public async cleanupFiles(): Promise<void> {\n    console.log(`[FileService cleanupFiles] Starting cleanup for directory: ${this.baseFilesPath}`);\n\n    this.debouncedCreateArchive.cancel();\n\n    try {\n      const archivePath = path.join(this.prebuiltArchiveDir, \"files.zip\");\n      if (fs.existsSync(archivePath)) {\n        await fs.promises.unlink(archivePath);\n        console.log(`[FileService cleanupFiles] Deleted archive file: ${archivePath}`);\n      }\n\n      const archiveDir = await fs.promises.readdir(this.prebuiltArchiveDir).catch(() => []);\n      for (const file of archiveDir) {\n        if (file.startsWith(\"files-\") && file.endsWith(\".zip.tmp\")) {\n          const tempFilePath = path.join(this.prebuiltArchiveDir, file);\n          await fs.promises.unlink(tempFilePath).catch((err) => {\n            console.error(\n              `[FileService cleanupFiles] Error deleting temp archive ${tempFilePath}:`,\n              err,\n            );\n          });\n        }\n      }\n    } catch (err: any) {\n      console.error(`[FileService cleanupFiles] Error cleaning up archive files:`, err);\n    }\n\n    try {\n      const files = await fs.promises.readdir(this.baseFilesPath);\n      for (const file of files) {\n        await fs.promises.rm(path.join(this.baseFilesPath, file), {\n          recursive: true,\n          force: true,\n        });\n      }\n      console.log(\n        `[FileService cleanupFiles] Cleared contents of directory: ${this.baseFilesPath}`,\n      );\n    } catch (err: any) {\n      if (err.code !== \"ENOENT\") {\n        console.error(\n          `[FileService cleanupFiles] Error cleaning directory ${this.baseFilesPath}:`,\n          err,\n        );\n      }\n    }\n    console.log(`[FileService cleanupFiles] Files cleaned. Creating empty archive.`);\n    this.debouncedCreateArchive();\n  }\n\n  public getBaseFilesPath(): string {\n    return this.baseFilesPath;\n  }\n\n  public async getPrebuiltArchivePath(): Promise<string> {\n    return this.prebuiltArchivePath;\n  }\n\n  private _createArchive(): Promise<string | null> {\n    return new Promise(async (resolvePromise, rejectPromise) => {\n      if (this.isArchiving) {\n        console.warn(\n          `[_createArchive] Warning: Archiving process initiated while another is already in progress. This might lead to conflicts if not handled by caller.`,\n        );\n      }\n\n      this.isArchiving = true;\n      console.log(`[_createArchive] Starting archive creation`);\n\n      const tempArchivePath = path.join(this.prebuiltArchiveDir, `files-${Date.now()}.zip.tmp`);\n      const finalArchivePath = path.join(this.prebuiltArchiveDir, \"files.zip\");\n\n      try {\n        await fs.promises.mkdir(this.prebuiltArchiveDir, { recursive: true });\n      } catch (mkdirError) {\n        console.error(\n          `[_createArchive] Error creating archive directory ${this.prebuiltArchiveDir}:`,\n          mkdirError,\n        );\n        this.isArchiving = false;\n        return rejectPromise(mkdirError);\n      }\n\n      const output = fs.createWriteStream(tempArchivePath);\n      const archive = archiver(\"zip\", { zlib: { level: 9 } });\n      let errorOccurredStream = false;\n\n      const operationCleanup = async (\n        success: boolean,\n        archivePath: string | null = null,\n        error?: any,\n      ) => {\n        this.isArchiving = false;\n\n        if (!success && tempArchivePath && (await this.exists(tempArchivePath))) {\n          try {\n            await fs.promises.unlink(tempArchivePath);\n            console.log(\"[_createArchive] Cleaned up temporary archive file due to error.\");\n          } catch (unlinkErr) {\n            console.error(\n              \"[_createArchive] Failed to clean up temp archive file after error:\",\n              unlinkErr,\n            );\n          }\n        }\n        if (success && archivePath) {\n          resolvePromise(archivePath);\n        } else {\n          rejectPromise(error || new Error(\"Archiving failed due to an unknown reason.\"));\n        }\n      };\n\n      output.on(\"close\", async () => {\n        if (errorOccurredStream) {\n          console.log(\n            \"[_createArchive] Output stream closed after an error was emitted and handled.\",\n          );\n          return;\n        }\n        try {\n          if (await this.exists(finalArchivePath)) {\n            await fs.promises.unlink(finalArchivePath);\n          }\n          await fs.promises.rename(tempArchivePath, finalArchivePath);\n          console.log(\n            `[_createArchive] Archive successfully created: ${finalArchivePath}, size: ${archive.pointer()} bytes`,\n          );\n          operationCleanup(true, finalArchivePath);\n        } catch (renameError) {\n          console.error(\"[_createArchive] Error renaming temporary archive file:\", renameError);\n          operationCleanup(false, null, renameError);\n        }\n      });\n\n      output.on(\"error\", (err) => {\n        console.error(\"[_createArchive] Archive output stream error:\", err);\n        errorOccurredStream = true;\n        if (!output.writableFinished) {\n          output.destroy();\n        }\n        operationCleanup(false, null, err);\n      });\n\n      archive.on(\"warning\", (err) => {\n        if (err.code === \"ENOENT\") {\n          console.warn(`[_createArchive] Archiving warning (ENOENT): ${err.message}`);\n        } else {\n          console.error(\"[_createArchive] Archiving warning:\", err);\n        }\n      });\n\n      archive.on(\"error\", (err) => {\n        console.error(\"[_createArchive] Archiving process error (archive.on('error')):\", err);\n        errorOccurredStream = true;\n        if (!output.writableFinished) {\n          output.destroy(err instanceof Error ? err : new Error(String(err)));\n        }\n        operationCleanup(false, null, err);\n      });\n\n      try {\n        if (!(await this.exists(this.baseFilesPath))) {\n          console.warn(\n            `[_createArchive] Base directory ${this.baseFilesPath} does not exist. Creating empty archive.`,\n          );\n        } else {\n          const stats = await fs.promises.stat(this.baseFilesPath);\n          if (!stats.isDirectory()) {\n            console.error(\n              `[_createArchive] Base path ${this.baseFilesPath} is not a directory. Creating empty archive.`,\n            );\n          } else {\n            const files = await fs.promises.readdir(this.baseFilesPath);\n            if (files.length === 0) {\n              console.log(\"[_createArchive] Base directory is empty. Creating empty archive.\");\n            } else {\n              archive.directory(this.baseFilesPath, false);\n            }\n          }\n        }\n        archive.pipe(output);\n        await archive.finalize();\n      } catch (err: any) {\n        console.error(\"[_createArchive] Error during archive preparation or finalization:\", err);\n        errorOccurredStream = true;\n        if (!output.writableFinished) {\n          output.destroy(err instanceof Error ? err : new Error(String(err)));\n        }\n        operationCleanup(false, null, err);\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "api/src/services/leveldb/localstorage.ts",
    "content": "import fs from \"fs/promises\";\nimport path from \"path\";\nimport os from \"os\";\nimport { Level } from \"level\";\nimport iconv from \"iconv-lite\";\nimport { copyDirectory } from \"../../utils/leveldb.js\";\n\n/**\n * Decode a Chrome-encoded string.\n *  - 0x00 prefix => UTF‑16‑LE encoded string\n *  - 0x01 prefix => ISO‑8859‑1 encoded string\n */\nfunction decodeString(raw: Buffer): { value: string; charset: string } {\n  if (!raw || raw.length === 0) {\n    throw new Error(\"Cannot decode empty buffer\");\n  }\n\n  const prefix = raw[0];\n  const payload = raw.subarray(1);\n\n  if (prefix === 0) {\n    try {\n      const decoded = iconv.decode(payload, \"utf16-le\");\n      return { value: decoded, charset: \"UTF-16-LE\" };\n    } catch (err: unknown) {\n      throw new Error(`Failed to decode UTF-16-LE: ${err}`);\n    }\n  } else if (prefix === 1) {\n    return { value: payload.toString(\"latin1\"), charset: \"ISO-8859-1\" };\n  }\n\n  throw new Error(`Unknown string encoding prefix: ${prefix}`);\n}\n\nexport interface StorageMetadata {\n  storageKey: string;\n  timestamp: Date;\n  size: number;\n}\n\nexport interface LocalStorageRecord {\n  storageKey: string;\n  scriptKey: string;\n  charset: string;\n  decoded: string;\n  mime?: string;\n  conversions?: string[];\n  jsonType?: string;\n  value?: unknown;\n}\n\nexport class LocalStoreDb {\n  private db: Level<Buffer, Buffer>;\n  public records: LocalStorageRecord[] = [];\n\n  private constructor(db: Level<Buffer, Buffer>) {\n    this.db = db;\n  }\n\n  public static async open(dir: string): Promise<LocalStoreDb> {\n    try {\n      const db = new Level<Buffer, Buffer>(dir, {\n        createIfMissing: false,\n        keyEncoding: \"buffer\",\n        valueEncoding: \"buffer\",\n      } as any);\n      // wait until open\n      await db.open();\n      return new LocalStoreDb(db);\n    } catch (err) {\n      // Attempt fallback: copy to temp directory and open there\n      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), \"chrome-leveldb-\"));\n      await copyDirectory(dir, tmpDir);\n      const db = new Level<Buffer, Buffer>(tmpDir, {\n        createIfMissing: false,\n        keyEncoding: \"buffer\",\n        valueEncoding: \"buffer\",\n      } as any);\n      await db.open();\n      return new LocalStoreDb(db);\n    }\n  }\n\n  public async load(): Promise<void> {\n    const META_PREFIX = Buffer.from(\"META:\");\n    const RECORD_PREFIX = Buffer.from(\"_\");\n\n    for await (const [keyBuf, valueBuf] of this.db.iterator() as any) {\n      const key: Buffer = keyBuf as Buffer;\n      const value: Buffer = valueBuf as Buffer;\n\n      if (key[0] === RECORD_PREFIX[0]) {\n        // Remove prefix\n        const withoutPrefix = key.subarray(1);\n        const nullIndex = withoutPrefix.indexOf(0);\n        if (nullIndex === -1) continue;\n\n        const storageKeyBytes = withoutPrefix.subarray(0, nullIndex);\n        const scriptKeyBytes = withoutPrefix.subarray(nullIndex + 1);\n\n        const storageKey = storageKeyBytes.toString();\n        let scriptKeyDecoded: { value: string; charset: string };\n        let valueDecoded: { value: string; charset: string };\n        try {\n          scriptKeyDecoded = decodeString(scriptKeyBytes);\n          valueDecoded = decodeString(valueBuf);\n        } catch {\n          continue;\n        }\n\n        this.records.push({\n          storageKey,\n          scriptKey: scriptKeyDecoded.value,\n          charset: valueDecoded.charset,\n          decoded: valueDecoded.value,\n        });\n      }\n    }\n  }\n\n  public close(): void {\n    // @ts-ignore types mismatch but close exists\n    if (this.db.status === \"open\") this.db.close().catch(() => {});\n  }\n}\n\nexport class ChromeLocalStorageReader {\n  /**\n   * Reads a Chrome Local Storage LevelDB directory and returns a nested record of items\n   * grouped by domain / storage key.\n   */\n  public static async readLocalStorage(\n    dir: string,\n  ): Promise<Record<string, Record<string, string>>> {\n    const ldb = await LocalStoreDb.open(dir);\n    try {\n      await ldb.load();\n      const result: Record<string, Record<string, string>> = {};\n      for (const rec of ldb.records) {\n        if (!result[rec.storageKey]) {\n          result[rec.storageKey] = {};\n        }\n        result[rec.storageKey][rec.scriptKey] = rec.decoded;\n      }\n      return result;\n    } finally {\n      ldb.close();\n    }\n  }\n}\n"
  },
  {
    "path": "api/src/services/leveldb/sessionstorage.ts",
    "content": "import fs from \"fs/promises\";\nimport path from \"path\";\nimport os from \"os\";\nimport { Level } from \"level\";\nimport iconv from \"iconv-lite\";\nimport { fileTypeFromBuffer } from \"file-type\";\nimport { copyDirectory } from \"../../utils/leveldb.js\";\n\n/**\n * Decode a UTF-16LE string\n */\nfunction decodeUTF16LE(raw: Buffer): string {\n  try {\n    return iconv.decode(raw, \"utf16-le\");\n  } catch (err: unknown) {\n    throw new Error(`Failed to decode UTF-16-LE: ${err}`);\n  }\n}\n\nexport interface SessionStorageRecord {\n  mapId: number;\n  origin: string;\n  key: string;\n  charset: string;\n  decoded: string;\n  mime?: string;\n  conversions?: string[];\n  jsonType?: string;\n  value?: unknown;\n}\n\nexport class SessionStoreDb {\n  private db: Level<Buffer, Buffer>;\n  public records: SessionStorageRecord[] = [];\n  private mapIdToOrigin: Map<number, string> = new Map();\n\n  private constructor(db: Level<Buffer, Buffer>) {\n    this.db = db;\n  }\n\n  public static async open(dir: string): Promise<SessionStoreDb> {\n    try {\n      const db = new Level<Buffer, Buffer>(dir, {\n        createIfMissing: false,\n        keyEncoding: \"buffer\",\n        valueEncoding: \"buffer\",\n      } as any);\n      // wait until open\n      await db.open();\n      return new SessionStoreDb(db);\n    } catch (err) {\n      // Attempt fallback: copy to temp directory and open there\n      const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), \"chrome-leveldb-\"));\n      await copyDirectory(dir, tmpDir);\n      const db = new Level<Buffer, Buffer>(tmpDir, {\n        createIfMissing: false,\n        keyEncoding: \"buffer\",\n        valueEncoding: \"buffer\",\n      } as any);\n      await db.open();\n      return new SessionStoreDb(db);\n    }\n  }\n\n  /**\n   * Load namespace records to map from mapId to origin (hostname)\n   */\n  private async loadNamespaceRecords(): Promise<void> {\n    const NAMESPACE_PREFIX = Buffer.from(\"namespace-\");\n\n    for await (const [keyBuf, valueBuf] of this.db.iterator() as any) {\n      const key: Buffer = keyBuf as Buffer;\n      const value: Buffer = valueBuf as Buffer;\n\n      if (key.indexOf(NAMESPACE_PREFIX) === 0) {\n        const keyStr = key.toString();\n        // Format: \"namespace-<uuid>-<hostname>\"\n        const parts = keyStr.split(\"-\");\n        if (parts.length < 3) continue;\n\n        // Get hostname from the remaining parts (in case hostname contains dashes)\n        const hostname = parts.slice(2).join(\"-\").replace(/\\/$/, \"\");\n\n        // Get map-id value\n        const mapId = parseInt(value.toString(), 10);\n        if (isNaN(mapId)) continue;\n\n        this.mapIdToOrigin.set(mapId, hostname);\n      }\n    }\n  }\n\n  public async load(): Promise<void> {\n    // First load namespace records to build mapId to origin mapping\n    await this.loadNamespaceRecords();\n\n    const MAP_PREFIX = Buffer.from(\"map-\");\n\n    for await (const [keyBuf, valueBuf] of this.db.iterator() as any) {\n      const key: Buffer = keyBuf as Buffer;\n      const value: Buffer = valueBuf as Buffer;\n\n      if (key.indexOf(MAP_PREFIX) === 0) {\n        const withoutPrefix = key.subarray(MAP_PREFIX.length);\n        const parts = withoutPrefix.toString().split(\"-\", 2);\n\n        if (parts.length !== 2) continue;\n\n        const mapId = parseInt(parts[0], 10);\n        if (isNaN(mapId)) continue;\n\n        const keyStr = parts[1];\n        let valueDecoded: string;\n\n        try {\n          valueDecoded = decodeUTF16LE(value);\n        } catch {\n          continue;\n        }\n\n        // Look up the origin from the mapId\n        const origin = this.mapIdToOrigin.get(mapId) || \"unknown-origin\";\n\n        this.records.push({\n          mapId,\n          origin,\n          key: keyStr,\n          charset: \"UTF-16-LE\",\n          decoded: valueDecoded,\n        });\n      }\n    }\n  }\n\n  public close(): void {\n    // @ts-ignore types mismatch but close exists\n    if (this.db.status === \"open\") this.db.close().catch(() => {});\n  }\n}\n\n/**\n * Process a session storage record to add metadata about its content\n */\nexport function processSessionRecord(record: SessionStorageRecord): SessionStorageRecord {\n  const result = { ...record };\n  const buffer = Buffer.from(record.decoded);\n  let mime = \"application/octet-stream\";\n  const conversions: string[] = [];\n  let jsonType = \"\";\n  let value: unknown = null;\n\n  // Try to parse as JSON\n  try {\n    value = JSON.parse(record.decoded);\n    mime = \"application/json\";\n\n    if (value === null) {\n      jsonType = \"null\";\n    } else if (Array.isArray(value)) {\n      jsonType = \"array\";\n    } else if (typeof value === \"object\") {\n      jsonType = \"object\";\n    } else if (typeof value === \"string\") {\n      jsonType = \"string\";\n    } else if (typeof value === \"number\") {\n      jsonType = \"number\";\n    } else if (typeof value === \"boolean\") {\n      jsonType = \"boolean\";\n    }\n  } catch {\n    // Not valid JSON, try to determine file type\n    try {\n      const quoted = JSON.stringify(record.decoded);\n      if (JSON.parse(quoted)) {\n        value = quoted;\n        mime = \"text/plain\";\n        conversions.push(\"JSON.stringify\");\n      }\n    } catch {\n      const b64 = buffer.toString(\"base64\");\n      value = JSON.stringify(b64);\n      conversions.push(\"buffer.toString('base64')\");\n      conversions.push(\"JSON.stringify\");\n\n      // Try to detect MIME type\n      fileTypeFromBuffer(buffer)\n        .then((type) => {\n          if (type) {\n            result.mime = type.mime;\n          }\n        })\n        .catch(() => {});\n    }\n  }\n\n  result.mime = mime;\n  result.conversions = conversions;\n  result.jsonType = jsonType;\n  result.value = value;\n\n  return result;\n}\n\nexport class ChromeSessionStorageReader {\n  /**\n   * Reads a Chrome Session Storage LevelDB directory and returns a nested record of items\n   * grouped by origin (hostname).\n   */\n  public static async readSessionStorage(\n    dir: string,\n  ): Promise<Record<string, Record<string, string>>> {\n    const sdb = await SessionStoreDb.open(dir);\n    try {\n      await sdb.load();\n      const result: Record<string, Record<string, string>> = {};\n\n      for (const rec of sdb.records) {\n        if (!result[rec.origin]) {\n          result[rec.origin] = {};\n        }\n        result[rec.origin][rec.key] = rec.decoded;\n      }\n\n      return result;\n    } finally {\n      sdb.close();\n    }\n  }\n}\n"
  },
  {
    "path": "api/src/services/selenium.service.ts",
    "content": "import { EventEmitter } from \"events\";\nimport { ChildProcess, spawn } from \"child_process\";\nimport { BrowserLauncherOptions, BrowserEvent, BrowserEventType } from \"../types/index.js\";\nimport path, { dirname } from \"path\";\nimport { FastifyBaseLogger } from \"fastify\";\nimport { fileURLToPath } from \"url\";\n\nexport class SeleniumService extends EventEmitter {\n  private seleniumProcess: ChildProcess | null = null;\n  private seleniumServerUrl: string = \"http://localhost:4444\";\n  private port: number = 4444;\n  private launchOptions?: BrowserLauncherOptions;\n  private logger: FastifyBaseLogger;\n\n  constructor(logger: FastifyBaseLogger) {\n    super();\n    this.logger = logger;\n  }\n\n  public async getChromeArgs(): Promise<string[]> {\n    const { options, userAgent } = this.launchOptions ?? {};\n    return [\n      \"disable-dev-shm-usage\",\n      \"no-sandbox\",\n      \"enable-javascript\",\n      userAgent ? `user-agent=${userAgent}` : \"\",\n      options?.proxyUrl ? `proxy-server=${options.proxyUrl}` : \"\",\n      ...(options?.args?.map((arg) => (arg.startsWith(\"--\") ? arg.slice(2) : arg)) || []),\n    ].filter(Boolean);\n  }\n\n  public async launch(launchOptions: BrowserLauncherOptions): Promise<void> {\n    this.launchOptions = launchOptions;\n\n    if (this.seleniumProcess) {\n      await this.close();\n    }\n\n    const projectRoot = path.resolve(dirname(fileURLToPath(import.meta.url)), \"../../\");\n    const seleniumServerPath = path.join(projectRoot, \"selenium\", \"server\", \"selenium-server.jar\");\n\n    const seleniumArgs = [\"-jar\", seleniumServerPath, \"standalone\"];\n\n    this.seleniumProcess = spawn(\"java\", seleniumArgs);\n    this.seleniumServerUrl = `http://localhost:${this.port}`;\n\n    this.seleniumProcess.stdout?.on(\"data\", (data) => {\n      this.logger.info(`Selenium stdout: ${data}`);\n      this.postLog({\n        type: BrowserEventType.Console,\n        text: JSON.stringify({ type: BrowserEventType.Console, message: `${data}` }),\n        timestamp: new Date(),\n      });\n    });\n\n    this.seleniumProcess.stderr?.on(\"data\", (data) => {\n      this.logger.error(`Selenium stderr: ${data}`);\n      this.postLog({\n        type: BrowserEventType.Error,\n        text: JSON.stringify({ type: BrowserEventType.Error, error: `${data}` }),\n        timestamp: new Date(),\n      });\n    });\n\n    this.seleniumProcess.on(\"close\", (code) => {\n      this.logger.info(`Selenium process exited with code ${code}`);\n      this.seleniumProcess = null;\n    });\n\n    await new Promise<void>((resolve, reject) => {\n      const timeout = setTimeout(() => {\n        reject(new Error(\"Selenium server failed to start within the timeout period\"));\n      }, 15000); // 15 seconds timeout\n\n      this.seleniumProcess!.stdout?.on(\"data\", (data) => {\n        if (data.toString().includes(\"Started Selenium Standalone\")) {\n          clearTimeout(timeout);\n          resolve();\n        }\n      });\n    });\n  }\n\n  public close(): void {\n    if (this.seleniumProcess) {\n      this.seleniumProcess.kill(\"SIGINT\");\n      this.seleniumProcess = null;\n    }\n  }\n\n  public getSeleniumServerUrl(): string {\n    return this.seleniumServerUrl;\n  }\n\n  private async postLog(browserLog: BrowserEvent) {\n    if (!this.launchOptions?.logSinkUrl) {\n      return;\n    }\n    await fetch(this.launchOptions.logSinkUrl, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify(browserLog),\n    });\n  }\n}\n"
  },
  {
    "path": "api/src/services/session.service.ts",
    "content": "import { FastifyBaseLogger } from \"fastify\";\nimport { mkdir } from \"fs/promises\";\nimport os from \"os\";\nimport path, { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { env } from \"../env.js\";\nimport { BrowserFingerprintWithHeaders } from \"fingerprint-generator\";\nimport { CredentialsOptions, SessionDetails } from \"../modules/sessions/sessions.schema.js\";\nimport { BrowserLauncherOptions, OptimizeBandwidthOptions } from \"../types/index.js\";\nimport { IProxyServer, ProxyServer } from \"../utils/proxy.js\";\nimport { getBaseUrl, getUrl } from \"../utils/url.js\";\nimport { CDPService } from \"./cdp/cdp.service.js\";\nimport { CookieData } from \"./context/types.js\";\nimport { FileService } from \"./file.service.js\";\nimport { SeleniumService } from \"./selenium.service.js\";\nimport { TimezoneFetcher } from \"./timezone-fetcher.service.js\";\nimport { deepMerge } from \"../utils/context.js\";\n\ntype Session = SessionDetails & {\n  completion: Promise<void>;\n  complete: (value: void) => void;\n  proxyServer: IProxyServer | undefined;\n};\n\nconst sessionStats = {\n  duration: 0,\n  eventCount: 0,\n  timeout: 0,\n  creditsUsed: 0,\n  proxyTxBytes: 0,\n  proxyRxBytes: 0,\n};\n\nconst defaultSession = {\n  status: \"idle\" as SessionDetails[\"status\"],\n  websocketUrl: getBaseUrl(\"ws\"),\n  debugUrl: getUrl(\"v1/sessions/debug\"),\n  debuggerUrl: getUrl(\"v1/devtools/inspector.html\"),\n  sessionViewerUrl: getBaseUrl(),\n  dimensions: { width: 1920, height: 1080 },\n  userAgent: \"\",\n  isSelenium: false,\n  proxy: \"\",\n  solveCaptcha: false,\n};\n\nexport type ProxyFactory = (proxyUrl: string) => Promise<IProxyServer> | IProxyServer;\n\nexport class SessionService {\n  private logger: FastifyBaseLogger;\n  private cdpService: CDPService;\n  private seleniumService: SeleniumService;\n  private fileService: FileService;\n  private timezoneFetcher: TimezoneFetcher;\n  public proxyFactory: ProxyFactory = (proxyUrl) => new ProxyServer(proxyUrl);\n\n  public pastSessions: Session[] = [];\n  public activeSession: Session;\n\n  constructor(config: {\n    cdpService: CDPService;\n    seleniumService: SeleniumService;\n    fileService: FileService;\n    logger: FastifyBaseLogger;\n  }) {\n    this.cdpService = config.cdpService;\n    this.seleniumService = config.seleniumService;\n    this.fileService = config.fileService;\n    this.logger = config.logger;\n    this.timezoneFetcher = new TimezoneFetcher(config.logger);\n    this.activeSession = {\n      id: uuidv4(),\n      createdAt: new Date().toISOString(),\n      ...defaultSession,\n      ...sessionStats,\n      userAgent: this.cdpService.getUserAgent() ?? \"\",\n      dimensions: this.cdpService.getDimensions(),\n      completion: Promise.resolve(),\n      complete: () => {},\n      proxyServer: undefined,\n    };\n  }\n\n  public async startSession(options: {\n    sessionId?: string;\n    proxyUrl?: string;\n    userAgent?: string;\n    sessionContext?: {\n      cookies?: CookieData[];\n      localStorage?: Record<string, Record<string, any>>;\n    };\n    isSelenium?: boolean;\n    fingerprint?: BrowserFingerprintWithHeaders;\n    logSinkUrl?: string;\n    userDataDir?: string;\n    persist?: boolean;\n    blockAds?: boolean;\n    optimizeBandwidth?: boolean | OptimizeBandwidthOptions;\n    extensions?: string[];\n    timezone?: string;\n    dimensions?: { width: number; height: number };\n    extra?: Record<string, Record<string, string>>;\n    credentials: CredentialsOptions;\n    skipFingerprintInjection?: boolean;\n    userPreferences?: Record<string, any>;\n    deviceConfig?: { device: \"desktop\" | \"mobile\" };\n    headless?: boolean;\n    dangerouslyLogRequestDetails?: boolean;\n  }): Promise<SessionDetails> {\n    const {\n      sessionId,\n      proxyUrl,\n      userAgent,\n      sessionContext,\n      extensions,\n      logSinkUrl,\n      dimensions,\n      fingerprint,\n      isSelenium,\n      blockAds,\n      optimizeBandwidth,\n      extra,\n      credentials,\n      skipFingerprintInjection,\n      userPreferences,\n      deviceConfig,\n      headless,\n      dangerouslyLogRequestDetails,\n    } = options;\n\n    // start fetching timezone as early as possible\n    let timezonePromise: Promise<string>;\n    if (options.timezone) {\n      timezonePromise = Promise.resolve(options.timezone);\n    } else {\n      timezonePromise = this.timezoneFetcher.getTimezone(\n        proxyUrl,\n        env.DEFAULT_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone,\n      );\n    }\n\n    // If dimensions not provided, get from CDP service\n    const finalDimensions = dimensions || this.cdpService.getDimensions();\n\n    await this.resetSessionInfo({\n      id: sessionId || uuidv4(),\n      status: \"live\",\n      proxy: proxyUrl,\n      solveCaptcha: false,\n      dimensions: finalDimensions,\n      isSelenium,\n    });\n\n    const userDataDir =\n      options.userDataDir || options.persist === true\n        ? path.join(dirname(fileURLToPath(import.meta.url)), \"..\", \"..\", \"user-data-dir\")\n        : env.CHROME_USER_DATA_DIR || path.join(os.tmpdir(), \"steel-chrome\");\n    await mkdir(userDataDir, { recursive: true });\n\n    if (proxyUrl) {\n      this.activeSession.proxyServer = await this.proxyFactory(proxyUrl);\n      await this.activeSession.proxyServer.listen();\n    }\n\n    const defaultUserPreferences = {\n      plugins: {\n        always_open_pdf_externally: true,\n        plugins_disabled: [\"Chrome PDF Viewer\"],\n      },\n    };\n\n    const mergedUserPreferences = userPreferences\n      ? deepMerge(defaultUserPreferences, userPreferences)\n      : defaultUserPreferences;\n\n    // Normalize optimizeBandwidth: true => enable all flags (except lists)\n    const normalizeOptimizeBandwidth = (\n      value: boolean | OptimizeBandwidthOptions | undefined,\n    ): OptimizeBandwidthOptions | undefined => {\n      if (value === true) {\n        return { blockImages: true, blockMedia: true, blockStylesheets: true };\n      }\n      if (value && typeof value === \"object\") {\n        return { ...value };\n      }\n      return undefined;\n    };\n\n    const normalizedOptimize = normalizeOptimizeBandwidth(optimizeBandwidth);\n\n    const browserLauncherOptions: BrowserLauncherOptions = {\n      options: {\n        headless: headless ?? env.CHROME_HEADLESS,\n        proxyUrl: this.activeSession.proxyServer?.url,\n      },\n      sessionContext,\n      userAgent,\n      blockAds,\n      fingerprint,\n      optimizeBandwidth: normalizedOptimize,\n      extensions: extensions || [],\n      logSinkUrl,\n      timezone: timezonePromise,\n      dimensions,\n      userDataDir,\n      userPreferences: mergedUserPreferences,\n      extra,\n      credentials,\n      skipFingerprintInjection,\n      deviceConfig,\n      dangerouslyLogRequestDetails,\n    };\n\n    if (isSelenium) {\n      await this.cdpService.shutdown();\n      await this.seleniumService.launch(browserLauncherOptions);\n\n      Object.assign(this.activeSession, {\n        websocketUrl: \"\",\n        debugUrl: \"\",\n        sessionViewerUrl: \"\",\n        userAgent:\n          userAgent ||\n          \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36\",\n        dimensions: this.cdpService.getDimensions(),\n      });\n\n      return this.activeSession;\n    } else {\n      await this.cdpService.startNewSession(browserLauncherOptions);\n\n      Object.assign(this.activeSession, {\n        websocketUrl: getBaseUrl(\"ws\"),\n        debugUrl: getUrl(\"v1/sessions/debug\"),\n        debuggerUrl: getUrl(\"v1/devtools/inspector.html\"),\n        sessionViewerUrl: getBaseUrl(),\n        userAgent:\n          this.cdpService.getUserAgent() ||\n          \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36\",\n        dimensions: this.cdpService.getDimensions(),\n      });\n    }\n\n    return this.activeSession;\n  }\n\n  public async endSession(): Promise<SessionDetails> {\n    this.activeSession.complete();\n    this.activeSession.status = \"released\";\n    this.activeSession.duration =\n      new Date().getTime() - new Date(this.activeSession.createdAt).getTime();\n\n    if (this.activeSession.proxyServer) {\n      this.activeSession.proxyTxBytes = this.activeSession.proxyServer.txBytes;\n      this.activeSession.proxyRxBytes = this.activeSession.proxyServer.rxBytes;\n    }\n\n    if (this.activeSession.isSelenium) {\n      this.seleniumService.close();\n      await this.cdpService.launch();\n    } else {\n      await this.cdpService.endSession();\n    }\n\n    const releasedSession = this.activeSession;\n\n    await this.resetSessionInfo({\n      id: uuidv4(),\n      status: \"idle\",\n    });\n\n    this.pastSessions.push(releasedSession);\n\n    return releasedSession;\n  }\n\n  private async resetSessionInfo(overrides?: Partial<SessionDetails>): Promise<SessionDetails> {\n    this.activeSession.complete();\n\n    await this.activeSession.proxyServer?.close(true);\n    this.activeSession.proxyServer = undefined;\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n    this.activeSession = {\n      id: uuidv4(),\n      ...defaultSession,\n      ...overrides,\n      ...sessionStats,\n      userAgent: this.cdpService.getUserAgent() ?? \"\",\n      createdAt: new Date().toISOString(),\n      completion: promise,\n      complete: resolve,\n      proxyServer: undefined,\n    };\n\n    return this.activeSession;\n  }\n\n  public setProxyFactory(factory: ProxyFactory) {\n    this.proxyFactory = factory;\n  }\n}\n"
  },
  {
    "path": "api/src/services/timezone-fetcher.service.ts",
    "content": "import { FastifyBaseLogger } from \"fastify\";\nimport axios, { AxiosError } from \"axios\";\nimport { HttpsProxyAgent } from \"https-proxy-agent\";\nimport { SocksProxyAgent } from \"socks-proxy-agent\";\nimport { env } from \"../env.js\";\n\nexport interface TimezoneFetchResult {\n  timezone: string | null;\n  error?: string;\n  service?: string;\n}\n\ninterface TimezoneService {\n  name: string;\n  url: string;\n  parseTimezone: (data: any) => string | null;\n}\n\nexport class TimezoneFetcher {\n  private logger: FastifyBaseLogger;\n  private fetchPromises: Map<string, Promise<TimezoneFetchResult>> = new Map();\n  private readonly FETCH_TIMEOUT = 2000;\n  private readonly services: TimezoneService[];\n\n  constructor(logger: FastifyBaseLogger) {\n    this.logger = logger;\n\n    // Use timezone service URL from env, default to ipinfo.io\n    const serviceUrl = env.TIMEZONE_SERVICE_URL || \"https://ipinfo.io/json\";\n\n    this.services = [\n      {\n        name: new URL(serviceUrl).hostname,\n        url: serviceUrl,\n        parseTimezone: (data) => data?.timezone || null,\n      },\n    ];\n  }\n\n  private startFetch(proxyUrl?: string): Promise<TimezoneFetchResult> {\n    const cacheKey = proxyUrl || \"direct\";\n    const existing = this.fetchPromises.get(cacheKey);\n    if (existing) {\n      return existing;\n    }\n\n    const fetchPromise = this.fetchTimezoneInternal(proxyUrl);\n\n    this.fetchPromises.set(cacheKey, fetchPromise);\n\n    fetchPromise.finally(() => {\n      this.fetchPromises.delete(cacheKey);\n    });\n\n    return fetchPromise;\n  }\n\n  public async getTimezone(proxyUrl?: string, fallback?: string): Promise<string> {\n    const startTime = Date.now();\n    try {\n      const result = await this.startFetch(proxyUrl);\n      if (result.timezone) {\n        const duration = Date.now() - startTime;\n        this.logger.info(\n          {\n            timezone: result.timezone,\n            service: result.service,\n            duration_ms: duration,\n            has_proxy: !!proxyUrl,\n          },\n          `[TimezoneFetcher] Successfully fetched timezone`,\n        );\n        return result.timezone;\n      }\n    } catch (error) {\n      const duration = Date.now() - startTime;\n      this.logger.warn(\n        {\n          duration_ms: duration,\n          has_proxy: !!proxyUrl,\n        },\n        `[TimezoneFetcher] Failed to fetch timezone: ${error}`,\n      );\n    }\n\n    const fallbackTimezone = fallback || Intl.DateTimeFormat().resolvedOptions().timeZone;\n    this.logger.info(`[TimezoneFetcher] Using fallback timezone: ${fallbackTimezone}`);\n    return fallbackTimezone;\n  }\n\n  private async fetchTimezoneInternal(proxyUrl?: string): Promise<TimezoneFetchResult> {\n    const agent = proxyUrl\n      ? proxyUrl.startsWith(\"socks\")\n        ? new SocksProxyAgent(proxyUrl)\n        : new HttpsProxyAgent(proxyUrl)\n      : undefined;\n\n    const servicePromises = this.services.map(async (service) => {\n      try {\n        const response = await axios.get(service.url, {\n          httpAgent: agent,\n          httpsAgent: agent,\n          timeout: this.FETCH_TIMEOUT,\n        });\n\n        const timezone = service.parseTimezone(response.data);\n\n        if (timezone) {\n          return {\n            timezone,\n            service: service.name,\n          };\n        } else {\n          throw new Error(`No timezone found in response from ${service.name}`);\n        }\n      } catch (error: unknown) {\n        const errorMessage = error instanceof AxiosError ? error.message : String(error);\n        this.logger.warn(`[TimezoneFetcher] ${service.name} failed: ${errorMessage}`);\n        throw new Error(`${service.name}: ${errorMessage}`);\n      }\n    });\n\n    try {\n      const result = await Promise.any(servicePromises);\n      return result;\n    } catch (error: unknown) {\n      const errorMessage =\n        error instanceof AggregateError\n          ? `All services failed: ${error.errors.map((e) => e.message).join(\", \")}`\n          : error instanceof Error\n          ? error.message\n          : String(error);\n\n      const context = proxyUrl ? `with proxy` : \"with direct connection\";\n      this.logger.warn(`[TimezoneFetcher] All services failed ${context}: ${errorMessage}`);\n\n      return {\n        timezone: null,\n        error: errorMessage,\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "api/src/services/websocket-registry.service.ts",
    "content": "import { WebSocketHandler, WebSocketHandlerRegistry } from \"../types/websocket.js\";\n\nexport class WebSocketRegistryService implements WebSocketHandlerRegistry {\n  public handlers = new Map<string, WebSocketHandler>();\n\n  registerHandler(handler: WebSocketHandler): void {\n    this.handlers.set(handler.path, handler);\n  }\n\n  getHandler(path: string): WebSocketHandler | undefined {\n    return this.handlers.get(path);\n  }\n\n  matchHandler(url: string): WebSocketHandler | undefined {\n    // TODO: use path-to-regexp or find-my-way to match the path\n    // Find the first handler whose path matches the start of the URL\n    for (const [path, handler] of this.handlers.entries()) {\n      if (url.startsWith(path)) {\n        return handler;\n      }\n    }\n    return undefined;\n  }\n}\n"
  },
  {
    "path": "api/src/steel-browser-plugin.ts",
    "content": "import fastifyView from \"@fastify/view\";\nimport { FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\nimport path, { dirname } from \"node:path\";\nimport browserInstancePlugin from \"./plugins/browser.js\";\nimport browserSessionPlugin from \"./plugins/browser-session.js\";\nimport browserWebSocket from \"./plugins/browser-socket/browser-socket.js\";\nimport customBodyParser from \"./plugins/custom-body-parser.js\";\nimport fileStoragePlugin from \"./plugins/file-storage.js\";\nimport requestLogger from \"./plugins/request-logger.js\";\nimport openAPIPlugin from \"./plugins/schemas.js\";\nimport seleniumPlugin from \"./plugins/selenium.js\";\nimport {\n  actionsRoutes,\n  cdpRoutes,\n  filesRoutes,\n  logsRoutes,\n  seleniumRoutes,\n  sessionsRoutes,\n} from \"./routes.js\";\nimport { fileURLToPath } from \"node:url\";\nimport ejs from \"ejs\";\nimport type { CDPService } from \"./services/cdp/cdp.service.js\";\nimport type { BrowserLauncherOptions } from \"./types/browser.js\";\nimport { WebSocketHandler } from \"./types/websocket.js\";\nimport { WebSocketRegistryService } from \"./services/websocket-registry.service.js\";\nimport { SessionService } from \"./services/session.service.js\";\nimport { LogStorage } from \"./services/cdp/instrumentation/storage/log-storage.interface.js\";\n\n// We need to redeclare any decorators from within the plugin that we want to expose\ndeclare module \"fastify\" {\n  interface FastifyInstance {\n    steelBrowserConfig: SteelBrowserConfig;\n    cdpService: CDPService;\n    sessionService: SessionService;\n    webSocketRegistry: WebSocketRegistryService;\n    registerCDPLaunchHook: (hook: (config: BrowserLauncherOptions) => Promise<void> | void) => void;\n    registerCDPShutdownHook: (\n      hook: (config: BrowserLauncherOptions | null) => Promise<void> | void,\n    ) => void;\n  }\n\n  interface LogStorageInterface extends LogStorage {}\n}\n\nexport interface SteelBrowserConfig {\n  fileStorage?: {\n    maxSizePerSession?: number;\n  };\n  customWsHandlers?: WebSocketHandler[];\n  logging?: {\n    enableStorage?: boolean;\n    storagePath?: string;\n    enableConsoleLogging?: boolean;\n    enableLogsRoutes?: boolean;\n  };\n}\n\nconst steelBrowserPlugin: FastifyPluginAsync<SteelBrowserConfig> = async (fastify, opts) => {\n  fastify.decorate(\"steelBrowserConfig\", opts);\n  // Plugins\n  await fastify.register(fastifyView, {\n    engine: {\n      ejs,\n    },\n    root: path.join(dirname(fileURLToPath(import.meta.url)), \"templates\"),\n  });\n  await fastify.register(requestLogger);\n  await fastify.register(openAPIPlugin);\n  await fastify.register(fileStoragePlugin);\n  await fastify.register(browserInstancePlugin);\n  await fastify.register(seleniumPlugin);\n  await fastify.register(browserWebSocket, {\n    customHandlers: opts.customWsHandlers,\n  });\n  await fastify.register(customBodyParser);\n  await fastify.register(browserSessionPlugin);\n\n  // Routes\n  await fastify.register(actionsRoutes, { prefix: \"/v1\" });\n  await fastify.register(sessionsRoutes, { prefix: \"/v1\" });\n  await fastify.register(cdpRoutes, { prefix: \"/v1\" });\n  await fastify.register(seleniumRoutes);\n  await fastify.register(filesRoutes, { prefix: \"/v1\" });\n\n  const enableLogsRoutes = opts.logging?.enableLogsRoutes ?? true;\n  if (enableLogsRoutes) {\n    await fastify.register(logsRoutes, { prefix: \"/v1/logs\" });\n  }\n};\n\nexport default fp<SteelBrowserConfig>(steelBrowserPlugin, {\n  name: \"steel-browser\",\n  fastify: \"5.x\",\n});\n"
  },
  {
    "path": "api/src/telemetry/noop.ts",
    "content": "import type { Span } from \"@opentelemetry/api\";\n\nexport const noopSpan: Span = {\n  spanContext() {\n    return {\n      traceId: \"\",\n      spanId: \"\",\n      traceFlags: 0,\n      isRemote: false,\n    };\n  },\n  setAttribute() {\n    return this;\n  },\n  setAttributes() {\n    return this;\n  },\n  addEvent() {\n    return this;\n  },\n  addLink() {\n    return this;\n  },\n  addLinks() {\n    return this;\n  },\n  setStatus() {\n    return this;\n  },\n  updateName() {\n    return this;\n  },\n  end() {},\n  isRecording() {\n    return false;\n  },\n  recordException() {},\n};\n"
  },
  {
    "path": "api/src/telemetry/tracer.ts",
    "content": "import type { Span, SpanOptions } from \"@opentelemetry/api\";\nimport { noopSpan } from \"./noop.js\";\n\nlet otel: typeof import(\"@opentelemetry/api\") | undefined;\ntry {\n  otel = await import(\"@opentelemetry/api\");\n} catch (err: any) {\n  if (err?.code !== \"MODULE_NOT_FOUND\" && err?.code !== \"ERR_MODULE_NOT_FOUND\") {\n    throw err;\n  }\n}\n\ninterface TracerOptions extends SpanOptions {\n  spanName?: string;\n  tracerName?: string;\n}\n\nexport const tracer = {\n  startActiveSpan<F extends (span: Span) => unknown>(\n    name: string,\n    fn: F,\n    opts?: Omit<TracerOptions, \"spanName\">,\n  ): ReturnType<F> {\n    if (!otel) {\n      return fn(noopSpan) as ReturnType<F>;\n    }\n\n    const { tracerName, ...options } = opts ?? {};\n    const rawTracer = otel.trace.getTracer(tracerName ?? \"steel\");\n\n    return rawTracer.startActiveSpan(name, options ?? {}, (span: Span) => {\n      try {\n        const result = fn(span);\n\n        if (result instanceof Promise) {\n          return result\n            .catch((error: Error) => {\n              span.recordException(error);\n              span.setStatus({\n                code: otel.SpanStatusCode.ERROR,\n                message: error.message,\n              });\n              throw error;\n            })\n            .finally(() => {\n              span.end();\n            }) as ReturnType<F>;\n        }\n\n        span.setStatus({ code: otel.SpanStatusCode.OK });\n        span.end();\n        return result as ReturnType<F>;\n      } catch (error: any) {\n        span.recordException(error);\n        span.setStatus({\n          code: otel.SpanStatusCode.ERROR,\n          message: error.message,\n        });\n        span.end();\n        throw error;\n      }\n    });\n  },\n  factory(tracerName: string) {\n    return {\n      startActiveSpan<F extends (span: Span) => unknown>(\n        name: string,\n        fn: F,\n        opts?: Omit<TracerOptions, \"spanName\" | \"tracerName\">,\n      ): ReturnType<F> {\n        return tracer.startActiveSpan(name, fn, { ...opts, tracerName });\n      },\n    };\n  },\n};\n\nexport function traceable(\n  target: Object,\n  propertyKey: string,\n  descriptor: PropertyDescriptor,\n): void;\nexport function traceable(opts?: TracerOptions): MethodDecorator;\nexport function traceable(\n  targetOrOpts?: any,\n  propertyKey?: string,\n  descriptor?: PropertyDescriptor,\n): any {\n  // Used as @traceable\n  if (typeof targetOrOpts === \"object\" && propertyKey !== undefined && descriptor !== undefined) {\n    return createDecorator()(targetOrOpts, propertyKey, descriptor);\n  }\n\n  // Used as @traceable({...})\n  return createDecorator(targetOrOpts as TracerOptions | undefined);\n}\n\nfunction createDecorator(opts?: TracerOptions) {\n  const { spanName, tracerName, ...options } = opts ?? {};\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    if (!otel) {\n      return descriptor;\n    }\n\n    const originalMethod = descriptor.value;\n\n    descriptor.value = function (...args: any[]) {\n      const tracername = tracerName ?? toKebabCase(target.constructor.name);\n      const name = spanName ?? `${target.constructor.name}.${propertyKey}`;\n\n      return tracer.startActiveSpan(\n        name,\n        () => {\n          return originalMethod.apply(this, args);\n        },\n        {\n          ...options,\n          tracerName: tracername,\n        },\n      );\n    };\n  };\n}\n\nfunction toKebabCase(str: string) {\n  return str\n    .replace(/([a-z0-9])([A-Z])/g, \"$1-$2\")\n    .replace(/([A-Z])([A-Z][a-z])/g, \"$1-$2\")\n    .toLowerCase();\n}\n"
  },
  {
    "path": "api/src/templates/live-session-streamer.ejs",
    "content": "<!DOCTYPE html>\n  <html data-theme=\"<%= theme %>\" <%= singlePageMode ? 'data-single-page-mode=\"true\"' : '' %>>\n<head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Steel Session Player</title>\n    <style>\n        /* CSS Variables for themes */\n        html[data-theme=\"dark\"] {\n              --bg-primary: #272725;\n              --bg-secondary: #171717;\n              --border-color: #383838;\n              --text-color: #ffffff;\n              --tab-active-bg: #272725;\n              --tab-hover-bg: #333333;\n              --icon-color: #8a8a8a;\n              --icon-hover-color: #ffffff;\n              --error-color: #e53935;\n              --offline-indicator-color: #e53935;\n              --loading-overlay-bg: rgba(30, 30, 30, 0.8);\n              --loading-spinner-color: #ffffff;\n        }\n\n        html[data-theme=\"light\"] {\n            --bg-primary: #ffffff;\n            --bg-secondary: #f5f5f5;\n            --border-color: #e0e0e0;\n            --text-color: #000000;\n              --tab-active-bg: #e8e8e8;\n              --tab-hover-bg: #efefef;\n              --icon-color: #666666;\n              --icon-hover-color: #000000;\n              --error-color: #e53935;\n              --offline-indicator-color: #e53935;\n              --loading-overlay-bg: rgba(240, 240, 240, 0.8);\n              --loading-spinner-color: #333333;\n        }\n\n        html, body {\n            margin: 0;\n            padding: 0;\n            width: 100%;\n            height: 100%;\n            background: var(--bg-primary);\n            overflow: hidden;\n              font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n        }\n        .container {\n            width: 100%;\n            height: 100%;\n            background: var(--bg-primary);\n            border: none;\n            display: flex;\n            flex-direction: column;\n            box-sizing: border-box;\n            overflow: hidden;\n        }\n        .top-bar {\n            height: 40px;\n            flex: 0 0 40px;\n            background: var(--bg-secondary);\n            border-bottom: 1px solid var(--border-color);\n            display: flex;\n            align-items: center;\n            justify-content: center;\n              padding: 0;\n            box-sizing: border-box;\n        }\n        .url-bar {\n              width: 100%;\n            height: 28px;\n            padding: 0 12px;\n            background: var(--bg-primary);\n            border-radius: 4px;\n            border: 1px solid var(--border-color);\n            display: flex;\n            align-items: center;\n            gap: 8px;\n            color: var(--text-color);\n            font-family: system-ui, -apple-system, sans-serif;\n            &:focus-within {\n              outline: none;\n              background: var(--tab-hover-bg);\n            }\n        }\n        .favicon {\n            width: 14px;\n            height: 14px;\n            object-fit: contain;\n            margin-right: 4px;\n        }\n        .url-input {\n            flex: 1;\n            border: none;\n            background: transparent;\n            color: var(--text-color);\n            font-family: 'Geist', sans-serif;\n            font-size: 13px;\n            outline: none;\n            width: 100%;\n        }\n        .content {\n            min-height: 0;\n            flex: 1;\n            overflow: hidden;\n            background: white;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            position: relative;\n        }\n          .canvas-container {\n              position: absolute;\n              height: 100%;\n              width: 100%;\n              display: none;\n          }\n          .canvas-container.active {\n              display: flex;\n              align-items: center;\n              justify-content: center;\n          }\n          .canvas {\n            position: absolute;\n            height: 100%;\n            width: auto;\n            left: 50%;\n            transform: translateX(-50%);\n            object-fit: contain;\n          }\n\n          /* Tab bar styles */\n          .tab-bar {\n            display: flex;\n            padding: 6px;\n            gap: 4px;\n            height: 36px;\n            background: var(--bg-secondary);\n            border-bottom: 1px solid var(--border-color);\n            overflow-x: auto;\n            scrollbar-width: none; /* Firefox */\n            -ms-overflow-style: none; /* IE and Edge */\n            align-items: center;\n          }\n          .tab-bar::-webkit-scrollbar {\n              display: none; /* Chrome, Safari, Opera */\n          }\n          .tab {\n              display: flex;\n              align-items: center;\n              padding: 0 12px;\n              min-width: 120px;\n              max-width: 200px;\n              height: 36px;\n              border-radius: 8px;\n              color: var(--text-color);\n              font-size: 12px;\n              cursor: <%= interactive ? \"pointer\" : \"default\" %>;\n              white-space: nowrap;\n              overflow: hidden;\n              text-overflow: ellipsis;\n              position: relative;\n              gap: 8px;\n              transition: background-color 0.2s;\n          }\n          .tab:hover {\n              background-color: <%= interactive ? \"var(--tab-hover-bg)\" : \"transparent\" %>;\n          }\n          .tab.active {\n              background-color: var(--tab-active-bg);\n          }\n          .tab-favicon {\n              width: 16px;\n              height: 16px;\n              object-fit: contain;\n          }\n          .tab-title {\n              flex: 1;\n              overflow: hidden;\n              text-overflow: ellipsis;\n          }\n          .tab-close {\n              width: 16px;\n              height: 16px;\n              display: flex;\n              align-items: center;\n              justify-content: center;\n              border-radius: 50%;\n              opacity: 0.6;\n              font-size: 14px;\n              line-height: 1;\n          }\n          .tab-close:hover {\n              background: rgba(255, 255, 255, 0.1);\n              opacity: 1;\n          }\n          .new-tab-button {\n              display: flex;\n              align-items: center;\n              justify-content: center;\n              width: 36px;\n              min-width: 36px;\n              height: 36px;\n              color: var(--icon-color);\n              cursor: pointer;\n              font-size: 18px;\n              transition: color 0.2s;\n          }\n          .new-tab-button:hover {\n              color: var(--icon-hover-color);\n          }\n\n          /* Navigation buttons */\n          .nav-buttons {\n              display: flex;\n              gap: 4px;\n              margin-left: 8px;\n              margin-right: 8px;\n          }\n        .nav-button {\n              width: 28px;\n              height: 28px;\n            border: none;\n            background: transparent;\n              color: var(--icon-color);\n            cursor: pointer;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n              font-size: 18px;\n            padding: 0;\n              border-radius: 4px;\n              transition: all 0.2s;\n        }\n        .nav-button:hover {\n              color: var(--icon-hover-color);\n              background: rgba(255, 255, 255, 0.1);\n          }\n          .nav-button:disabled {\n              cursor: default;\n              &:hover {\n                background: transparent;\n              }\n          }\n\n          /* Browser chrome layout */\n          .browser-chrome {\n              display: flex;\n              flex-direction: column;\n              width: 100%;\n              position: relative;\n          }\n          .address-bar {\n              display: flex;\n              align-items: center;\n              padding: 0 8px;\n              height: 40px;\n              background: var(--bg-secondary);\n              border-bottom: 1px solid var(--border-color);\n          }\n\n          /* Icons */\n          .icon {\n              width: 16px;\n              height: 16px;\n              fill: var(--icon-color);\n          }\n          .icon-container {\n              display: flex;\n              align-items: center;\n              justify-content: center;\n          }\n\n          /* Offline status indicator styling - inline with tabs */\n          .connection-status {\n              display: flex;\n              align-items: center;\n              padding: 0 12px;\n              height: 36px;\n              color: var(--text-color);\n              font-family: system-ui, -apple-system, sans-serif;\n              font-size: 13px;\n              box-sizing: border-box;\n              min-width: 140px;\n              flex-shrink: 0;\n          }\n\n          .connection-status.offline {\n              display: flex;\n          }\n\n          .connection-status.online {\n              display: none;\n          }\n\n          /* Initially hide the connection status until we know the state */\n          .connection-status.connecting {\n              display: none;\n          }\n\n          .status-indicator {\n              width: 8px;\n              height: 8px;\n              border-radius: 50%;\n              margin-right: 8px;\n              display: inline-block;\n              flex-shrink: 0;\n          }\n\n          .status-indicator.offline {\n              background-color: var(--offline-indicator-color);\n          }\n\n          /* Remove the margin adjustment since it's now inline */\n          .connection-status.offline + .tab-bar {\n              margin-top: 0;\n          }\n\n          /* Add new styles for the lock icons */\n          .url-security-icon {\n              width: 18px;\n              height: 18px;\n              display: flex;\n              align-items: center;\n              justify-content: center;\n          }\n\n          .url-security-icon svg {\n              width: 18px;\n              height: 18px;\n              fill: var(--icon-color);\n          }\n\n          .url-security-icon.secure svg {\n              fill: #4CAF50; /* Green for secure connections */\n          }\n\n          /* Single-page mode styles */\n          html[data-single-page-mode=\"true\"] .tab-bar {\n              display: none !important;\n          }\n\n          html[data-single-page-mode=\"true\"] .content {\n              height: calc(100% - 40px); /* Only account for address bar in single-page mode */\n          }\n\n          /* Add styles for WebSocket connection loading/error states */\n          .canvas-container.loading::before {\n              content: \"Loading...\";\n              position: absolute;\n              top: 50%;\n              left: 50%;\n              transform: translate(-50%, -50%);\n              color: var(--text-color);\n              font-family: system-ui, -apple-system, sans-serif;\n              font-size: 16px;\n              z-index: 5;\n          }\n\n          .canvas-container.error::before {\n              content: \"Session released\";\n              position: absolute;\n              top: 50%;\n              left: 50%;\n              transform: translate(-50%, -50%);\n              color: #fff;\n              font-family: system-ui, -apple-system, sans-serif;\n              font-size: 16px;\n              z-index: 5;\n          }\n\n          /* Tab switching loading overlay */\n          .canvas-container.tab-switching::after {\n              content: \"\";\n              position: absolute;\n              top: 0;\n              left: 0;\n              right: 0;\n              bottom: 0;\n              background: var(--loading-overlay-bg);\n              z-index: 10;\n          }\n\n          .canvas-container.tab-switching::before {\n              content: \"\";\n              position: absolute;\n              width: 40px;\n              height: 40px;\n              top: 50%;\n              left: 50%;\n              transform: translate(-50%, -50%);\n              border: 4px solid transparent;\n              border-top-color: var(--loading-spinner-color);\n              border-radius: 50%;\n              animation: spin 1s linear infinite;\n              z-index: 11;\n          }\n\n          @keyframes spin {\n              0% { transform: translate(-50%, -50%) rotate(0deg); }\n              100% { transform: translate(-50%, -50%) rotate(360deg); }\n          }\n\n          /* Tab loading spinner */\n          .tab-favicon-spinner {\n              width: 16px;\n              height: 16px;\n              display: none;\n              position: relative;\n          }\n\n          .tab-favicon-spinner::after {\n              content: '';\n              position: absolute;\n              width: 12px;\n              height: 12px;\n              top: 2px;\n              left: 2px;\n              border: 2px solid var(--icon-color);\n              border-top-color: transparent;\n              border-radius: 50%;\n              animation: spinner-rotation 0.8s linear infinite;\n          }\n\n          @keyframes spinner-rotation {\n              0% { transform: rotate(0deg); }\n              100% { transform: rotate(360deg); }\n          }\n\n          .tab.loading .tab-favicon {\n              display: none;\n          }\n\n          .tab.loading .tab-favicon-spinner {\n              display: block;\n          }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <% if (showControls) { %>\n          <div class=\"browser-chrome\">\n              <% if (!singlePageMode) { %>\n              <div class=\"tab-bar\" id=\"tab-bar\">\n                  <div class=\"connection-status connecting\" id=\"connection-status\">\n                      <div class=\"status-indicator offline\"></div>\n                      <span>Session Offline</span>\n                  </div>\n                  <!-- Tabs will be dynamically added here -->\n                  <!-- New tab button commented out as requested\n                  <div class=\"new-tab-button\" id=\"new-tab-button\">+</div>\n                  -->\n              </div>\n              <% } %>\n              <div class=\"address-bar\">\n                <div class=\"nav-buttons\">\n                    <button class=\"nav-button\" id=\"back-button\" <%= interactive ? \"\" : \"disabled\" %>>\n                        <svg class=\"icon\" viewBox=\"0 0 24 24\">\n                            <path d=\"M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z\"/>\n                        </svg>\n                    </button>\n                    <button class=\"nav-button\" id=\"forward-button\" <%= interactive ? \"\" : \"disabled\" %>>\n                        <svg class=\"icon\" viewBox=\"0 0 24 24\">\n                            <path d=\"M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z\"/>\n                        </svg>\n                    </button>\n                    <button class=\"nav-button\" id=\"refresh-button\" <%= interactive ? \"\" : \"disabled\" %>>\n                        <svg class=\"icon\" viewBox=\"0 0 24 24\">\n                            <path d=\"M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z\"/>\n                        </svg>\n                    </button>\n                </div>\n                <div class=\"url-bar\">\n                    <div class=\"url-security-icon\" id=\"url-security-icon\">\n                        <svg viewBox=\"0 0 24 24\" id=\"lock-icon\" style=\"display: none;\">\n                            <path d=\"M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z\"/>\n                        </svg>\n                        <svg viewBox=\"0 0 24 24\" id=\"unlock-icon\" style=\"display: none;\">\n                            <path d=\"M12 17c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 12H6V10h12v10z\"/>\n                        </svg>\n                    </div>\n                    <input type=\"text\" id=\"url-text\" class=\"url-input\" value=\"Session connecting...\" disabled <%= interactive ? \"\" : \"readonly\" %>>\n                </div>\n              </div>\n          </div>\n        <% } %>\n        <div class=\"content\" id=\"content\">\n            <!-- Canvas containers will be dynamically added here -->\n        </div>\n    </div>\n\n    <script>\n          // Global flag for single page mode\n          const singlePageMode = <%= singlePageMode %>;\n          const interactive = <%= interactive %>;\n\n          // Tab and page management\n          const tabBar = document.getElementById('tab-bar');\n          const contentContainer = document.getElementById('content');\n          const urlText = document.getElementById('url-text');\n          const backButton = document.getElementById('back-button');\n          const forwardButton = document.getElementById('forward-button');\n          const refreshButton = document.getElementById('refresh-button');\n          const connectionStatus = document.getElementById('connection-status');\n\n          let tabs = {};\n          let activeTabId = null;\n\n          // WebSocket connection management\n          const baseWsUrl = '<%= wsUrl %>';\n          let tabDiscoveryWebSocket = null; // WebSocket for discovering tabs (multi-page mode)\n          let activeConnectionRetries = {}; // Track reconnection attempts\n\n          // Default dimensions until we get first image\n          const defaultWidth = <%= dimensions?.width || 1920 %>;\n          const defaultHeight = <%= dimensions?.width || 1080 %>;\n\n          // Function to set connection status\n          function setConnectionStatus(online) {\n              if (connectionStatus) {\n                  if (online) {\n                      // Connected - remove both connecting and offline classes\n                      connectionStatus.classList.remove('connecting');\n                      connectionStatus.classList.remove('offline');\n                      connectionStatus.classList.add('online');\n                  } else {\n                      // Disconnected/error - show the offline status\n                      connectionStatus.classList.remove('connecting');\n                      connectionStatus.classList.remove('online');\n                      connectionStatus.classList.add('offline');\n                      updateUrlBar('Session not connected');\n                  }\n              }\n          }\n\n          // Function to create WebSocket URL for a specific page\n          function createWebSocketUrl(pageId) {\n              // For single-page mode, the URL already includes the pageId\n              if (singlePageMode) return baseWsUrl;\n\n              // For tab discovery, add tabInfo parameter\n              if (pageId === 'tab-discovery') {\n                  return `${baseWsUrl}?tabInfo=true`;\n              }\n\n              // Regular tab connection\n              return `${baseWsUrl}?pageId=${encodeURIComponent(pageId)}`;\n          }\n\n          // Function to establish WebSocket connection for a tab\n          function connectTabWebSocket(pageId) {\n              // Prevent reconnection if already in progress\n              if (tabs[pageId] && tabs[pageId].reconnecting) {\n                  console.log(`Reconnection for tab ${pageId} already in progress, ignoring duplicate request`);\n                  return;\n              }\n\n              // If tab has an existing connection and it's open or connecting, don't create a new one\n              if (tabs[pageId] && tabs[pageId].websocket) {\n                  const ws = tabs[pageId].websocket;\n                  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {\n                      console.log(`Tab ${pageId} already has an active connection, not reconnecting`);\n                      return ws;\n                  }\n\n                  // Only attempt to close if the socket is not already closing or closed\n                  if (ws.readyState !== WebSocket.CLOSING && ws.readyState !== WebSocket.CLOSED) {\n                      try {\n                          ws.close();\n                      } catch (err) {\n                          console.error(`Error closing existing WebSocket for tab ${pageId}: ${err}`);\n                      }\n                  }\n              }\n\n              // Mark tab as loading and reconnecting\n              if (tabs[pageId]) {\n                  tabs[pageId].canvasContainer.classList.add('loading');\n                  tabs[pageId].canvasContainer.classList.remove('error');\n                  tabs[pageId].reconnecting = true;\n              }\n\n              // Create a new WebSocket for this tab\n              const ws = new WebSocket(createWebSocketUrl(pageId));\n              console.log(`Connecting websocket for tab ${pageId}`);\n\n              if (tabs[pageId]) {\n                  tabs[pageId].websocket = ws;\n              }\n\n              // Set up event handlers\n              ws.onopen = () => {\n                  console.log(`WebSocket connection opened for tab ${pageId}`);\n\n                  // Clear reconnection status\n                  if (tabs[pageId]) {\n                      tabs[pageId].reconnecting = false;\n                      tabs[pageId].canvasContainer.classList.remove('loading');\n                      tabs[pageId].canvasContainer.classList.remove('error');\n\n                      // Reset retry counter on successful connection\n                      activeConnectionRetries[pageId] = 0;\n\n                      if (pageId !== 'tab-discovery') {\n                          // Remove loading indicator\n                          tabs[pageId].canvasContainer.classList.remove('loading');\n                          tabs[pageId].canvasContainer.classList.remove('error');\n\n                          // Set up canvas event listeners immediately when connection opens\n                          if (tabs[pageId].canvas) {\n                              setupCanvasEventListeners(tabs[pageId].canvas, pageId, ws);\n                          }\n                      }\n\n                      // Update connection status if this is the active tab\n                      if (activeTabId === pageId) {\n                          setConnectionStatus(true);\n                      }\n                  }\n              };\n\n              ws.onclose = () => {\n                  console.log(`WebSocket connection closed for tab ${pageId}`);\n\n                  if (tabs[pageId]) {\n                      // Only show error if this wasn't an intentional close\n                      if (!tabs[pageId].intentionalClose) {\n                          tabs[pageId].canvasContainer.classList.add('error');\n                          // Only set connection status to offline if this is the active tab\n                          // and it's not a normal shutdown\n                          if (activeTabId === pageId) {\n                              setConnectionStatus(false);\n                          }\n                      }\n                      tabs[pageId].reconnecting = false;\n                      tabs[pageId].intentionalClose = false;\n                  }\n              };\n\n              // Add error handler to explicitly handle connection failures\n              ws.onerror = () => {\n                  console.log(`WebSocket connection error for tab ${pageId}`);\n\n                  if(pageId === 'tab-discovery') {\n                    setConnectionStatus(false);\n                    updateUrlBar('Session not connected');\n                  }\n\n                  if (tabs[pageId] && activeTabId === pageId) {\n                      // Mark connection as offline on error\n                      setConnectionStatus(false);\n                  }\n              };\n\n              ws.onmessage = (event) => {\n                  const payload = JSON.parse(event.data);\n\n                  // Handle tab-discovery messages\n                  if (pageId === 'tab-discovery') {\n                      if (payload.type === \"tabList\") {\n                          handleTabList(payload.tabs, payload.firstTabId);\n                          return;\n                      } else if (payload.type === \"tabClosed\") {\n                          handleTabClosed(payload.pageId);\n                          return;\n                      } else if (payload.type === \"activeTabChange\") {\n                        // Handle tab activation from server\n                        if (tabs[payload.pageId] && activeTabId !== payload.pageId) {\n                            activateTab(payload.pageId);\n                        }\n                        return;\n                    }\n                      return;\n                  }\n\n                  // For regular tabs, handle data\n                  if (payload.type === \"tabUpdate\") {\n                      updateTabInfo(pageId, payload.url, payload.title, payload.favicon);\n                      return;\n                  } else if (payload.type === \"targetClosed\") {\n                      handleTabClosed(pageId);\n                      return;\n                  }\n\n                  // Handle canvas image data\n                  if (payload.data) {\n                      if (!tabs[pageId]) {\n                          // console.error(`Received data for nonexistent tab ${pageId}`);\n                          return;\n                      }\n\n                      // Remove tab switching indicator when we receive the first frame\n                      tabs[pageId].canvasContainer.classList.remove('tab-switching');\n\n                      // Mark that we've received at least one frame for this tab\n                      tabs[pageId].receivedFirstFrame = true;\n                      if(urlText) urlText.disabled = false;\n\n                      if (urlText && document.activeElement !== urlText){\n                        updateUrlBar(payload.url);\n                        updateSecurityIcon(payload.url);\n                        updateTabInfo(pageId, payload.url, payload.title, payload.favicon);\n                      }\n\n                      // Track frame count for loading state\n                      if (tabs[pageId].isLoading) {\n                          tabs[pageId].frameCount++;\n\n                          // After receiving 2 frames, turn off loading state\n                          if (tabs[pageId].frameCount >= 2) {\n                              setTabLoadingState(pageId, false);\n                          }\n                      }\n                      window.parent.postMessage({\n                          type: 'navigation',\n                          url: payload.url,\n                          favicon: payload.favicon\n                      }, '*');\n\n                const img = new Image();\n                      const imageData = 'data:image/jpeg;base64,' + payload.data;\n                      tabs[pageId].lastImageData = imageData;\n\n                img.onload = () => {\n                          tabs[pageId].currentImageWidth = img.naturalWidth;\n                          tabs[pageId].currentImageHeight = img.naturalHeight;\n                          updateCanvasSize(pageId);\n\n                          tabs[pageId].ctx.drawImage(\n                        img,\n                        0,\n                        0,\n                              Math.floor(tabs[pageId].canvas.width / window.devicePixelRatio),\n                              Math.floor(tabs[pageId].canvas.height / window.devicePixelRatio)\n                          );\n\n                          tabs[pageId].canvasContainer.style.backgroundColor = 'var(--bg-secondary)';\n                      };\n\n                      img.src = imageData;\n                  }\n              };\n\n              return ws;\n          }\n\n          // Function to create a new tab\n          function createTab(pageId, url = '', title = 'New Tab', favicon = null) {\n              if (tabs[pageId]) {\n                  console.log(`Tab ${pageId} already exists, activating it instead of creating a new one`);\n                  activateTab(pageId);\n                  return;\n              }\n\n              // Create tab UI elements\n              const tab = document.createElement('div');\n              tab.className = 'tab';\n              tab.setAttribute('data-page-id', pageId);\n              if(!interactive) tab.setAttribute('disabled', 'true');\n\n              // Add favicon\n              const faviconElement = document.createElement('img');\n              faviconElement.className = 'tab-favicon';\n              if (favicon) {\n                  faviconElement.src = favicon;\n              } else {\n                  faviconElement.style.display = 'none';\n              }\n              tab.appendChild(faviconElement);\n\n              // Add loading spinner (initially hidden)\n              const spinnerElement = document.createElement('div');\n              spinnerElement.className = 'tab-favicon-spinner';\n              tab.appendChild(spinnerElement);\n\n              // Add title\n              const titleElement = document.createElement('div');\n              titleElement.className = 'tab-title';\n              titleElement.textContent = title || 'New Tab';\n              tab.appendChild(titleElement);\n\n              // Add close button if not in single-page mode\n              if (!singlePageMode) {\n                  const closeButton = document.createElement('div');\n                  closeButton.className = 'tab-close';\n                  closeButton.innerHTML = '&times;';\n                  closeButton.onclick = (e) => {\n                      e.stopPropagation();\n                      if(interactive) closeTab(pageId);\n                  };\n                  tab.appendChild(closeButton);\n              }\n\n              // Add click handler to switch tabs\n              tab.onclick = () => {\n                  if(interactive) activateTab(pageId);\n              };\n\n              // Add tab to tab bar\n              const tabBar = document.getElementById('tab-bar');\n              if (tabBar) {\n                  tabBar.appendChild(tab);\n              }\n\n              // Create canvas container\n              const canvasContainer = document.createElement('div');\n              canvasContainer.className = 'canvas-container';\n              if (document.getElementById('content')) {\n                  document.getElementById('content').appendChild(canvasContainer);\n              }\n\n              // Create canvas element\n              const canvas = document.createElement('canvas');\n              canvas.className = 'canvas';\n              canvasContainer.appendChild(canvas);\n\n              // Initialize canvas context here\n              const ctx = canvas.getContext('2d');\n\n              // Store tab information including ctx\n              tabs[pageId] = {\n                  tab,\n                  canvas,\n                  ctx,\n                  canvasContainer,\n                  url,\n                  title,\n                  favicon,\n                  websocket: null,\n                  reconnecting: false,\n                  intentionalClose: false,\n                  currentImageWidth: defaultWidth,\n                  currentImageHeight: defaultHeight,\n                  lastImageData: null,\n                  receivedFirstFrame: false,\n                  isLoading: false,\n                  frameCount: 0,\n                  lastNavigationUrl: null\n              };\n\n              return tabs[pageId];\n          }\n\n          // Function to activate a tab\n          function activateTab(pageId) {\n              if (!tabs[pageId]) {\n                  console.error(`Attempted to activate nonexistent tab ${pageId}`);\n                  return;\n              }\n\n              // Deactivate the current active tab if there is one\n              if (activeTabId && tabs[activeTabId]) {\n                  tabs[activeTabId].tab.classList.remove('active');\n                  tabs[activeTabId].canvasContainer.classList.remove('active');\n\n                  // Intentionally close the WebSocket for the inactive tab if it exists to save resources\n                  if (tabs[activeTabId].websocket &&\n                      tabs[activeTabId].websocket.readyState === WebSocket.OPEN) {\n                      tabs[activeTabId].intentionalClose = true;\n                      tabs[activeTabId].websocket.close();\n                      tabs[activeTabId].websocket = null;\n                      console.log(`Closed WebSocket for inactive tab ${activeTabId}`);\n                  }\n              }\n\n              // Set the new active tab\n              activeTabId = pageId;\n              tabs[pageId].tab.classList.add('active');\n              tabs[pageId].canvasContainer.classList.add('active');\n\n              // Update URL and security status based on active tab\n              if (tabs[pageId].url) {\n                  updateUrlBar(tabs[pageId].url);\n                  updateSecurityIcon(tabs[pageId].url);\n              }\n\n              // Don't change connection status on tab switch - maintain current status\n              // We'll update it once we confirm the actual connection state\n\n              // Add tab-switching loading indicator if we haven't received a frame yet\n              if (!tabs[pageId].receivedFirstFrame) {\n                  tabs[pageId].canvasContainer.classList.add('tab-switching');\n              }\n\n              // Ensure the WebSocket is connected for the newly activated tab\n              if (!tabs[pageId].websocket ||\n                  tabs[pageId].websocket.readyState === WebSocket.CLOSED ||\n                  tabs[pageId].websocket.readyState === WebSocket.CLOSING) {\n                  connectTabWebSocket(pageId);\n              } else if (tabs[pageId].websocket && tabs[pageId].websocket.readyState === WebSocket.OPEN) {\n                  // WebSocket is already open, set up canvas event listeners\n                  setupCanvasEventListeners(tabs[pageId].canvas, pageId, tabs[pageId].websocket);\n\n                  // Since the WebSocket is open, update the connection status to true\n                  setConnectionStatus(true);\n              }\n\n              // Update canvas size for the active tab\n              updateCanvasSize(pageId);\n          }\n\n          // Function to attempt reconnection for a tab WebSocket\n          function attemptReconnect(pageId) {\n              // Make sure tab still exists\n              if (!tabs[pageId]) return;\n\n              // Prevent multiple reconnection attempts\n              if (tabs[pageId].reconnecting) {\n                  console.log(`Already attempting to reconnect tab ${pageId}, ignoring duplicate request`);\n                  return;\n              }\n\n              // Initialize retry counter if not set\n              if (!activeConnectionRetries[pageId]) {\n                  activeConnectionRetries[pageId] = 0;\n              }\n\n              const maxRetries = 5;\n              const retryDelay = Math.min(2000 * Math.pow(1.5, activeConnectionRetries[pageId]), 30000);\n\n              // Add exponential backoff with jitter to prevent synchronized reconnection attempts\n              const jitter = Math.random() * 1000;\n              const delay = retryDelay + jitter;\n\n              if (activeConnectionRetries[pageId] < maxRetries) {\n                  console.log(`Attempting to reconnect ${pageId} in ${Math.round(delay)}ms (attempt ${activeConnectionRetries[pageId] + 1}/${maxRetries})`);\n\n                  // Mark tab as reconnecting to prevent duplicate attempts\n                  tabs[pageId].reconnecting = true;\n\n                  // If this is the active tab, update the connection status to show we're offline\n                  if (activeTabId === pageId) {\n                      setConnectionStatus(false);\n                  }\n\n                  setTimeout(() => {\n                      activeConnectionRetries[pageId]++;\n\n                      // Reconnect only if the tab still exists and we should still be reconnecting\n                      if (tabs[pageId]) {\n                          connectTabWebSocket(pageId);\n                      }\n                  }, delay);\n              } else {\n                  console.error(`Maximum retry attempts (${maxRetries}) reached for ${pageId}`);\n\n                  // Mark tab as error state but clear reconnecting flag\n                  if (tabs[pageId]) {\n                      tabs[pageId].reconnecting = false;\n                      tabs[pageId].canvasContainer.classList.remove('loading');\n                      tabs[pageId].canvasContainer.classList.add('error');\n\n                      // Ensure offline status is shown after max retries\n                      if (activeTabId === pageId) {\n                          setConnectionStatus(false);\n                      }\n                  }\n              }\n          }\n\n          // Set up WebSocket-related event listeners for a canvas\n          function setupCanvasEventListeners(canvas, pageId, ws) {\n              if (!canvas || !ws || !interactive) return;\n\n              // Remove any existing event listeners first to prevent duplicates\n              canvas.removeEventListener('mousedown', canvas._mouseDownHandler);\n              canvas.removeEventListener('mouseup', canvas._mouseUpHandler);\n              canvas.removeEventListener('mousemove', canvas._mouseMoveHandler);\n              canvas.removeEventListener('wheel', canvas._wheelHandler);\n\n              // Helper function for scaled coordinates\n              function getScaledCoordinates(e, canvas, pageId) {\n                  if (!tabs[pageId]) return { x: 0, y: 0 };\n\n                  const tabData = tabs[pageId];\n                  const rect = canvas.getBoundingClientRect();\n                  const canvasWidth = rect.width;\n                  const canvasHeight = rect.height;\n\n                  // Calculate scaling factors\n                  const scaleX = tabData.currentImageWidth / canvasWidth;\n                  const scaleY = tabData.currentImageHeight / canvasHeight;\n\n                  // Calculate the canvas offset from the left edge of the viewport\n                  const canvasLeftOffset = rect.left;\n\n                  // Calculate scaled coordinates relative to the canvas\n                  const x = Math.round((e.clientX - canvasLeftOffset) * scaleX);\n                  const y = Math.round((e.clientY - rect.top) * scaleY);\n\n                  // Ensure coordinates are within bounds\n                  return {\n                      x: Math.max(0, Math.min(x, tabData.currentImageWidth)),\n                      y: Math.max(0, Math.min(y, tabData.currentImageHeight))\n                  };\n              }\n\n              // MouseDown event\n              canvas._mouseDownHandler = (e) => {\n                  if (ws.readyState !== WebSocket.OPEN) return;\n\n                  const coords = getScaledCoordinates(e, canvas, pageId);\n\n                  ws.send(JSON.stringify({\n                      type: 'mouseEvent',\n                      pageId: pageId,\n                      event: {\n                          type: 'mousePressed',\n                          x: coords.x,\n                          y: coords.y,\n                          button: e.button === 0 ? 'left' : e.button === 1 ? 'middle' : 'right',\n                          modifiers: (e.ctrlKey ? 2 : 0) | (e.shiftKey ? 8 : 0) | (e.altKey ? 1 : 0) | (e.metaKey ? 4 : 0),\n                          clickCount: e.detail\n                      }\n                  }));\n\n                  console.log(`Mouse down at ${coords.x},${coords.y} with button ${e.button}`);\n              };\n              canvas.addEventListener('mousedown', canvas._mouseDownHandler);\n\n              // MouseUp event\n              canvas._mouseUpHandler = (e) => {\n                  if (ws.readyState !== WebSocket.OPEN) return;\n\n                  const coords = getScaledCoordinates(e, canvas, pageId);\n\n                  ws.send(JSON.stringify({\n                      type: 'mouseEvent',\n                      pageId: pageId,\n                      event: {\n                          type: 'mouseReleased',\n                          x: coords.x,\n                          y: coords.y,\n                          button: e.button === 0 ? 'left' : e.button === 1 ? 'middle' : 'right',\n                          modifiers: (e.ctrlKey ? 2 : 0) | (e.shiftKey ? 8 : 0) | (e.altKey ? 1 : 0) | (e.metaKey ? 4 : 0),\n                          clickCount: e.detail\n                      }\n                  }));\n\n                  console.log(`Mouse up at ${coords.x},${coords.y} with button ${e.button}`);\n              };\n              canvas.addEventListener('mouseup', canvas._mouseUpHandler);\n\n              // MouseMove event\n              canvas._mouseMoveHandler = (e) => {\n                  if (ws.readyState !== WebSocket.OPEN) return;\n\n                  const coords = getScaledCoordinates(e, canvas, pageId);\n\n                  ws.send(JSON.stringify({\n                      type: 'mouseEvent',\n                      pageId: pageId,\n                      event: {\n                          type: 'mouseMoved',\n                          x: coords.x,\n                          y: coords.y,\n                          button: 'none',\n                          modifiers: (e.ctrlKey ? 2 : 0) | (e.shiftKey ? 8 : 0) | (e.altKey ? 1 : 0) | (e.metaKey ? 4 : 0)\n                      }\n                  }));\n              };\n\n              // Debounce function to limit how often events are sent\n              function debounce(func, wait) {\n                  let timeout;\n                  return function(...args) {\n                      const context = this;\n                      clearTimeout(timeout);\n                      timeout = setTimeout(() => func.apply(context, args), wait);\n                  };\n              }\n\n              // Create debounced version of mouseMoveHandler\n              canvas._debouncedMouseMoveHandler = debounce((e) => {\n                  if (ws.readyState !== WebSocket.OPEN) return;\n\n                  const coords = getScaledCoordinates(e, canvas, pageId);\n\n                  ws.send(JSON.stringify({\n                      type: 'mouseEvent',\n                      pageId: pageId,\n                      event: {\n                          type: 'mouseMoved',\n                          x: coords.x,\n                          y: coords.y,\n                          button: 'none',\n                          modifiers: (e.ctrlKey ? 2 : 0) | (e.shiftKey ? 8 : 0) | (e.altKey ? 1 : 0) | (e.metaKey ? 4 : 0)\n                      }\n                  }));\n              }, 20); // 20ms debounce time - adjust as needed for performance vs responsiveness\n\n              // Attach the debounced handler to mousemove event\n              canvas.addEventListener('mousemove', canvas._debouncedMouseMoveHandler);\n\n              // Wheel event\n              canvas._wheelHandler = (e) => {\n                  if (ws.readyState !== WebSocket.OPEN) return;\n\n                  const coords = getScaledCoordinates(e, canvas, pageId);\n\n                  ws.send(JSON.stringify({\n                      type: 'mouseEvent',\n                      pageId: pageId,\n                      event: {\n                          type: 'mouseWheel',\n                          x: coords.x,\n                          y: coords.y,\n                          button: 'none',\n                          modifiers: (e.ctrlKey ? 2 : 0) | (e.shiftKey ? 8 : 0) | (e.altKey ? 1 : 0) | (e.metaKey ? 4 : 0),\n                          deltaX: e.deltaX,\n                          deltaY: e.deltaY\n                      }\n                  }));\n\n                  // Prevent scrolling the page\n                  e.preventDefault();\n              };\n              canvas.addEventListener('wheel', canvas._wheelHandler);\n\n              console.log(`Event listeners set up for canvas of tab ${pageId}`);\n              return true;\n          }\n\n          // Add tab navigation and keyboard event listeners (global)\n          if (interactive) {\n              document.addEventListener('keydown', (e) => {\n                  // Skip if URL input is focused\n                  if (urlText && document.activeElement === urlText){\n                    console.log(\"URL input is focused, skipping keydown event\");\n                    return;\n                  }\n\n                  if (!activeTabId || !tabs[activeTabId] || !tabs[activeTabId].websocket) return;\n\n                  const ws = tabs[activeTabId].websocket;\n                  if (ws.readyState !== WebSocket.OPEN) return;\n\n                  ws.send(JSON.stringify({\n                      type: 'keyEvent',\n                      pageId: activeTabId,\n                      event: {\n                          type: 'keyDown',\n                          text: e.key.length === 1 ? e.key : undefined,\n                          code: e.code,\n                          key: e.key,\n                          keyCode: e.keyCode\n                      }\n                  }));\n              });\n\n              document.addEventListener('keyup', (e) => {\n                  // Skip if URL input is focused\n                  if (urlText && document.activeElement === urlText) return;\n                  if (!activeTabId || !tabs[activeTabId] || !tabs[activeTabId].websocket) return;\n\n                  const ws = tabs[activeTabId].websocket;\n                  if (ws.readyState !== WebSocket.OPEN) return;\n\n                  ws.send(JSON.stringify({\n                      type: 'keyEvent',\n                      pageId: activeTabId,\n                      event: {\n                          type: 'keyUp',\n                          text: e.key.length === 1 ? e.key : undefined,\n                          code: e.code,\n                          key: e.key,\n                          keyCode: e.keyCode\n                      }\n                  }));\n              });\n\n              if (urlText) {\n                  urlText.addEventListener('keydown', (e) => {\n                      if (e.key === 'Enter') {\n                          e.preventDefault();\n                          e.stopPropagation(); // Stop event propagation to prevent the document keydown handler from firing\n                          if (!activeTabId || !tabs[activeTabId] || !tabs[activeTabId].websocket) return;\n\n                          const url = urlText.value;\n                          updateSecurityIcon(url);\n\n                          const ws = tabs[activeTabId].websocket;\n                          if (ws.readyState !== WebSocket.OPEN) return;\n\n                          // Set tab loading state when navigation starts\n                          setTabLoadingState(activeTabId, true, url);\n\n                          ws.send(JSON.stringify({\n                              type: 'navigation',\n                              pageId: activeTabId,\n                              event: {\n                                  url: url\n                              }\n                          }));\n\n                          // Send message to parent frame about manual URL change\n                          window.parent.postMessage({\n                              type: 'navigation',\n                              url: url\n                          }, '*');\n\n                          urlText.blur();\n                      }\n                  });\n\n                  // Add keyup event listener to URL input to prevent bubbling to document\n                  urlText.addEventListener('keyup', (e) => {\n                      e.stopPropagation(); // Prevent the event from bubbling up to document\n                  });\n              }\n\n              if (backButton) {\n                  backButton.addEventListener('click', () => {\n                      if (!activeTabId || !tabs[activeTabId] || !tabs[activeTabId].websocket) return;\n\n                      const ws = tabs[activeTabId].websocket;\n                      if (ws.readyState !== WebSocket.OPEN) return;\n\n                      // Set tab loading state when navigation starts\n                      setTabLoadingState(activeTabId, true);\n\n                      ws.send(JSON.stringify({\n                          type: 'navigation',\n                          pageId: activeTabId,\n                          event: {\n                              action: 'back'\n                          }\n                      }));\n                  });\n              }\n\n              if (forwardButton) {\n                  forwardButton.addEventListener('click', () => {\n                      if (!activeTabId || !tabs[activeTabId] || !tabs[activeTabId].websocket) return;\n\n                      const ws = tabs[activeTabId].websocket;\n                      if (ws.readyState !== WebSocket.OPEN) return;\n\n                      // Set tab loading state when navigation starts\n                      setTabLoadingState(activeTabId, true);\n\n                      ws.send(JSON.stringify({\n                          type: 'navigation',\n                          pageId: activeTabId,\n                          event: {\n                              action: 'forward'\n                          }\n                      }));\n                  });\n              }\n\n              if (refreshButton) {\n                  refreshButton.addEventListener('click', () => {\n                      if (!activeTabId || !tabs[activeTabId] || !tabs[activeTabId].websocket) return;\n\n                      const ws = tabs[activeTabId].websocket;\n                      if (ws.readyState !== WebSocket.OPEN) return;\n\n                      // Set tab loading state when navigation starts\n                      setTabLoadingState(activeTabId, true);\n\n                      ws.send(JSON.stringify({\n                          type: 'navigation',\n                          pageId: activeTabId,\n                          event: {\n                              action: 'refresh'\n                          }\n                      }));\n                  });\n              }\n          }\n\n          // Function to handle tab list updates from server\n          function handleTabList(tabList, firstTabId = null) {\n              if (!tabList || !Array.isArray(tabList)) return;\n\n              // console.log(\"Received tab list:\", tabList);\n\n              // Create any tabs that don't exist\n              tabList.forEach((tab, index) => {\n                  if (!tabs[tab.id]) {\n                      // Make sure we pass parameters in the right order: id, url, title, favicon\n                      createTab(tab.id, tab.url || '', tab.title || 'New Tab', tab.favicon || null);\n                      if (index === tabList.length - 1) {\n                          activateTab(tab.id);\n                      }\n                  } else {\n                      // Update existing tab info\n                      updateTabInfo(tab.id, tab.url, tab.title, tab.favicon);\n                  }\n              });\n\n              // Remove tabs that are no longer present\n              const currentPageIds = tabList.map(tab => tab.id);\n              Object.keys(tabs).forEach(pageId => {\n                  if (pageId !== 'tab-discovery' && !currentPageIds.includes(pageId)) {\n                      // Only remove DOM elements, don't send closeTab message since\n                      // the server already knows this tab is closed\n                      closeTab(pageId);\n                  }\n              });\n\n              // If no tab is active but tabs exist, activate the first tab\n              // or activate the specified firstTabId if provided\n              if ((!activeTabId || !tabs[activeTabId] || activeTabId === 'tab-discovery') && tabList.length > 0) {\n                  // If firstTabId is provided, use it; otherwise use the first tab in the list\n                  const tabToActivate = firstTabId && tabs[firstTabId] ? firstTabId : tabList[0].id;\n                  activateTab(tabToActivate);\n              }\n          }\n\n          // Function to update tab info\n          function updateTabInfo(pageId, url, title, favicon) {\n              if (!tabs[pageId]) {\n                  console.error(`Attempted to update tab ${pageId} which doesn't exist`);\n                  return;\n              }\n\n              // console.log(`Updating tab ${pageId}:`, {url, title, favicon});\n\n              const tabData = tabs[pageId];\n\n              // Update the tab's stored data\n              if (url !== undefined) tabData.url = url;\n              if (title !== undefined) tabData.title = title;\n              if (favicon !== undefined) tabData.favicon = favicon;\n\n              // Update the tab's title element\n              const titleElem = tabData.tab.querySelector('.tab-title');\n              if (titleElem) {\n                  titleElem.textContent = tabData.title || 'New Tab';\n              }\n\n              // Update favicon if available\n              const faviconElem = tabData.tab.querySelector('.tab-favicon');\n              if (faviconElem && tabData.favicon) {\n                  faviconElem.src = tabData.favicon;\n                  faviconElem.style.display = 'block';\n              } else if (faviconElem) {\n                  faviconElem.style.display = 'none';\n              }\n\n              // Only update URL bar and security icon if this is the active tab\n              if (activeTabId === pageId) {\n                  updateUrlBar(tabData.url || '');\n                  updateSecurityIcon(tabData.url || '');\n              }\n          }\n\n          // Function to handle tab closed by server\n          function handleTabClosed(pageId) {\n              if (!tabs[pageId]) return;\n              closeTab(pageId);\n          }\n\n          // Function to close a tab manually\n          function closeTab(pageId) {\n              if (!tabs[pageId]) return;\n\n              const tabIndexes = Object.keys(tabs).filter(id => id !== 'tab-discovery');\n\n              if (tabIndexes.length <= 1) {\n                  return;\n              }\n\n              const tabIndex = tabIndexes.indexOf(pageId);\n\n              console.log(`Closing tab ${pageId}`);\n\n              // Intentionally close the WebSocket\n              if (tabs[pageId].websocket) {\n                  tabs[pageId].intentionalClose = true;\n                  tabs[pageId].websocket.send(JSON.stringify({\n                      type: 'closeTab',\n                      pageId: pageId\n                  }));\n                  tabs[pageId].websocket.close();\n                  tabs[pageId].websocket = null;\n              }\n\n              // Remove tab from DOM\n              if (tabs[pageId].tab) {\n                  tabs[pageId].tab.remove();\n              }\n\n              // Remove canvas container from DOM\n              if (tabs[pageId].canvasContainer) {\n                  tabs[pageId].canvasContainer.remove();\n              }\n\n              // If this was the active tab, activate another tab if available\n              if (activeTabId === pageId) {\n                  const tabIds = Object.keys(tabs).filter(id => id !== 'tab-discovery');\n                  if (tabIds.length > 0) {\n                      // Activate the last available tab\n                      const tabIndex = tabIds.indexOf(pageId);\n                      activateTab(tabIds[Math.min(tabIndex + 1, tabIds.length - 1)]);\n                  } else {\n                      // No tabs left, clear active tab ID\n                      activeTabId = null;\n                      setConnectionStatus(false);\n                  }\n              }\n\n              // Delete tab from tabs object\n              delete tabs[pageId];\n          }\n\n          // Initialize connection - in single-page mode we have only one connection\n          if (singlePageMode) {\n              // In single-page mode, create a single tab with no ID (will use URL parameter)\n              createTab('default');\n              activateTab('default');\n          } else {\n              // In multi-page mode, first connect to the tab discovery service\n              connectTabWebSocket('tab-discovery');\n          }\n\n          // Set initial security icon state\n          updateSecurityIcon('');\n\n          // Function to update the URL input field\n          function updateUrlBar(url) {\n              const urlInput = document.getElementById('url-input');\n              if (urlInput && urlInput.value !== url) {\n                  urlInput.value = url || '';\n              } else {\n                  const urlText = document.getElementById('url-text');\n                  if (urlText && urlText.value !== url) {\n                      urlText.value = url || 'Session not connected';\n                  }\n              }\n          }\n\n          // Add the updateSecurityIcon function that's being referenced but was missing\n          function updateSecurityIcon(url) {\n              const lockIcon = document.getElementById('lock-icon');\n              const unlockIcon = document.getElementById('unlock-icon');\n              const securityIcon = document.getElementById('url-security-icon');\n\n              if (!lockIcon || !unlockIcon || !securityIcon) return;\n\n              // Case-insensitive check for https\n              if (url && (url.toLowerCase().startsWith('https://') || url.toLowerCase().startsWith('https:'))) {\n                  lockIcon.style.display = 'block';\n                  unlockIcon.style.display = 'none';\n                  securityIcon.classList.add('secure');\n              } else {\n                  lockIcon.style.display = 'none';\n                  unlockIcon.style.display = 'block';\n                  securityIcon.classList.remove('secure');\n              }\n          }\n\n          // Add the missing updateCanvasSize function\n          function updateCanvasSize(pageId) {\n              if (!tabs[pageId]) return;\n\n              const tabData = tabs[pageId];\n              const container = tabData.canvasContainer;\n              const canvas = tabData.canvas;\n              const ctx = tabData.ctx;\n\n              if (!ctx) {\n                  console.error(`Canvas context not initialized for tab ${pageId}`);\n                  return;\n              }\n\n              const parentHeight = container.clientHeight;\n\n              // Scale to height while maintaining aspect ratio\n              const targetHeight = parentHeight;\n              const targetWidth = targetHeight * (tabData.currentImageWidth / tabData.currentImageHeight);\n\n              // Account for device pixel ratio\n              const dpr = window.devicePixelRatio || 1;\n\n              canvas.width = targetWidth * dpr;\n              canvas.height = targetHeight * dpr;\n\n              // Reset transform before scaling\n              ctx.setTransform(1, 0, 0, 1, 0, 0);\n              ctx.scale(dpr, dpr);\n\n              // Set display size\n              canvas.style.height = '100%';\n              canvas.style.width = 'auto';\n\n              // Redraw the last image if available\n              if (tabData.lastImageData) {\n                  const img = new Image();\n                  img.onload = () => {\n                      ctx.drawImage(\n                          img,\n                          0,\n                          0,\n                          Math.floor(canvas.width / window.devicePixelRatio),\n                          Math.floor(canvas.height / window.devicePixelRatio)\n                      );\n                  };\n                  img.src = tabData.lastImageData;\n              }\n          }\n\n          // Add a function to handle tab loading state\n          function setTabLoadingState(pageId, isLoading, navigationUrl = null) {\n              if (!tabs[pageId]) return;\n              // If we're starting to load\n              if (isLoading) {\n                  tabs[pageId].isLoading = true;\n                  tabs[pageId].frameCount = 0;\n                  tabs[pageId].lastNavigationUrl = navigationUrl || tabs[pageId].url;\n                  tabs[pageId].tab.classList.add('loading');\n              } else {\n                  // If we're finishing loading\n                  tabs[pageId].isLoading = false;\n                  tabs[pageId].tab.classList.remove('loading');\n              }\n          }\n\n          // Global clipboard variables\n          let clipboardRequestId = 0;\n          const pendingRequests = new Map();\n\n          (function() {\n              // Check if clipboard bridge is enabled\n              const urlParams = new URLSearchParams(window.location.search);\n              const clipboardBridgeEnabled = urlParams.get('clipboardBridge') === 'true';\n\n              if (!clipboardBridgeEnabled || !interactive) {\n                  return;\n              }\n\n              window.parent.postMessage({\n                  type: 'clipboardBridgeReady'\n              }, '*');\n\n              // Handle messages from parent window\n              window.addEventListener('message', (event) => {\n                  switch (event.data.type) {\n                      case 'triggerCopy':\n                          handleCopyEvent();\n                          break;\n                      case 'triggerPaste':\n                          handlePasteEvent(event.data.text);\n                          break;\n                      case 'clipboardReadResponse':\n                      case 'clipboardWriteResponse':\n                          handleClipboardResponse(event.data);\n                          break;\n                  }\n              });\n\n              function handleCopyEvent() {\n                  if (!activeTabId || !tabs[activeTabId] || !tabs[activeTabId].websocket) {\n                      return;\n                  }\n\n                  const ws = tabs[activeTabId].websocket;\n                  if (ws.readyState !== WebSocket.OPEN) {\n                      return;\n                  }\n\n                  ws.send(JSON.stringify({\n                      type: 'getSelectedText',\n                      pageId: activeTabId\n                  }));\n              }\n\n              function handlePasteEvent(text) {\n                  if (!text || !activeTabId || !tabs[activeTabId] || !tabs[activeTabId].websocket) {\n                      return;\n                  }\n\n                  const ws = tabs[activeTabId].websocket;\n                  if (ws.readyState !== WebSocket.OPEN) {\n                      return;\n                  }\n\n                  // Send each character as individual key events\n                  for (let i = 0; i < text.length; i++) {\n                      const char = text[i];\n                      const charCode = char.charCodeAt(0);\n\n                      ws.send(JSON.stringify({\n                          type: 'keyEvent',\n                          pageId: activeTabId,\n                          event: {\n                              type: 'keyDown',\n                              text: char,\n                              code: getKeyCode(char),\n                              key: char,\n                              keyCode: charCode\n                          }\n                      }));\n\n                      ws.send(JSON.stringify({\n                          type: 'keyEvent',\n                          pageId: activeTabId,\n                          event: {\n                              type: 'keyUp',\n                              text: char,\n                              code: getKeyCode(char),\n                              key: char,\n                              keyCode: charCode\n                          }\n                      }));\n                  }\n              }\n\n              function getKeyCode(char) {\n                  // Simple key code mapping for common characters\n                  if (char === ' ') return 'Space';\n                  if (char === '\\n') return 'Enter';\n                  if (char === '\\t') return 'Tab';\n                  if (/[a-zA-Z]/.test(char)) return `Key${char.toUpperCase()}`;\n                  if (/[0-9]/.test(char)) return `Digit${char}`;\n                  return `Key${char.toUpperCase()}`;\n              }\n\n              function handleClipboardResponse(data) {\n                  const request = pendingRequests.get(data.requestId);\n                  if (request) {\n                      pendingRequests.delete(data.requestId);\n\n                      if (data.type === 'clipboardWriteResponse') {\n                          if (!data.success) {\n                            console.error('Clipboard write failed:', data.error);\n                          }\n                      } else if (data.type === 'clipboardReadResponse') {\n                          if (data.text) {\n                              handlePasteEvent(data.text);\n                          } else {\n                              console.error('Clipboard read failed:', data.error);\n                          }\n                      }\n                  }\n              }\n\n              // Override existing keyboard handlers to intercept copy/paste\n              const originalKeydownHandler = document.onkeydown;\n\n              document.addEventListener('keydown', (e) => {\n                  // Skip if URL input is focused\n                  if (urlText && document.activeElement === urlText) {\n                      return;\n                  }\n\n                  const isCtrlOrCmd = e.ctrlKey || e.metaKey;\n\n                  if (isCtrlOrCmd && (e.key === 'c' || e.key === 'C')) {\n                      e.preventDefault();\n                      e.stopPropagation();\n                      handleCopyEvent();\n                      return false;\n                  }\n\n                  if (isCtrlOrCmd && (e.key === 'v' || e.key === 'V')) {\n                      e.preventDefault();\n                      e.stopPropagation();\n\n                      const requestId = ++clipboardRequestId;\n                      pendingRequests.set(requestId, { type: 'read' });\n\n                      window.parent.postMessage({\n                          type: 'requestClipboardRead',\n                          requestId: requestId\n                      }, '*');\n\n                      return false;\n                  }\n\n                  // For other keys, continue with normal processing\n                  return true;\n              }, true);\n          })();\n\n          const originalConnectTabWebSocket = connectTabWebSocket;\n          connectTabWebSocket = function(pageId) {\n              const ws = originalConnectTabWebSocket(pageId);\n\n              const originalOnMessage = ws.onmessage;\n              ws.onmessage = function(event) {\n                  const payload = JSON.parse(event.data);\n\n                  if (payload.type === \"selectedTextResponse\") {\n\n                      if (payload.text && payload.text.length > 0) {\n                          const requestId = ++clipboardRequestId;\n                          pendingRequests.set(requestId, { type: 'write', text: payload.text });\n\n                          window.parent.postMessage({\n                              type: 'requestClipboardWrite',\n                              text: payload.text,\n                              requestId: requestId\n                          }, '*');\n                      } else {\n                          console.log('No text selected in browser');\n                      }\n                      return;\n                  }\n\n                  originalOnMessage.call(this, event);\n              };\n\n              return ws;\n          };\n    </script>\n</body>\n</html>\n"
  },
  {
    "path": "api/src/types/browser.ts",
    "content": "import type { BrowserEventType } from \"./enums.js\";\nimport type {\n  CookieData,\n  IndexedDBDatabase,\n  LocalStorageData,\n  SessionStorageData,\n} from \"../services/context/types.js\";\nimport { BrowserFingerprintWithHeaders } from \"fingerprint-generator\";\nimport type { CredentialsOptions } from \"../modules/sessions/sessions.schema.js\";\n\nexport type OptimizeBandwidthOptions = {\n  blockImages?: boolean;\n  blockMedia?: boolean;\n  blockStylesheets?: boolean;\n  blockHosts?: string[];\n  blockUrlPatterns?: string[];\n};\n\nexport interface BrowserLauncherOptions {\n  options: BrowserServerOptions;\n  req?: Request;\n  stealth?: boolean;\n  sessionContext?: {\n    cookies?: CookieData[];\n    localStorage?: Record<string, LocalStorageData>;\n    sessionStorage?: Record<string, SessionStorageData>;\n    indexedDB?: Record<string, IndexedDBDatabase[]>;\n  };\n  userAgent?: string;\n  extensions?: string[];\n  logSinkUrl?: string;\n  blockAds?: boolean;\n  fingerprint?: BrowserFingerprintWithHeaders;\n  optimizeBandwidth?: boolean | OptimizeBandwidthOptions;\n  customHeaders?: Record<string, string>;\n  timezone?: Promise<string>;\n  dimensions?: {\n    width: number;\n    height: number;\n  } | null;\n  userDataDir?: string;\n  userPreferences?: Record<string, any>;\n  extra?: Record<string, Record<string, string>>;\n  credentials?: CredentialsOptions;\n  skipFingerprintInjection?: boolean;\n  deviceConfig?: { device: \"desktop\" | \"mobile\" };\n  dangerouslyLogRequestDetails?: boolean;\n}\n\nexport interface BrowserServerOptions {\n  args?: string[];\n  chromiumSandbox?: boolean;\n  devtools?: boolean;\n  downloadsPath?: string;\n  headless?: boolean;\n  ignoreDefaultArgs?: boolean | string[];\n  proxyUrl?: string;\n  timeout?: number;\n  tracesDir?: string;\n}\n\nexport type BrowserEvent = {\n  type: BrowserEventType;\n  text: string;\n  timestamp: Date;\n};\n"
  },
  {
    "path": "api/src/types/casting.ts",
    "content": "export type MouseEvent = {\n  type: \"mouseEvent\";\n  pageId: string;\n  event: {\n    type: \"mousePressed\" | \"mouseReleased\" | \"mouseWheel\" | \"mouseMoved\";\n    x: number;\n    y: number;\n    button: \"none\" | \"left\" | \"middle\" | \"right\";\n    modifiers: number;\n    clickCount?: number;\n    deltaX?: number;\n    deltaY?: number;\n  };\n};\n\nexport type KeyEvent = {\n  type: \"keyEvent\";\n  pageId: string;\n  event: {\n    type: \"keyDown\" | \"keyUp\" | \"char\";\n    text?: string;\n    code: string;\n    key: string;\n    keyCode: number;\n    modifiers?: number;\n  };\n};\n\nexport type NavigationEvent = {\n  type: \"navigation\";\n  pageId: string;\n  event: {\n    url?: string;\n    action?: \"back\" | \"forward\" | \"refresh\";\n  };\n};\n\nexport type CloseTabEvent = {\n  type: \"closeTab\";\n  pageId: string;\n};\n\nexport type ClipboardWriteEvent = {\n  type: \"clipboardWrite\";\n  pageId: string;\n  event: {\n    text: string;\n  };\n};\n\nexport type ClipboardReadEvent = {\n  type: \"clipboardRead\";\n  pageId: string;\n};\n\nexport type GetSelectedTextEvent = {\n  type: \"getSelectedText\";\n  pageId: string;\n};\n\nexport type PageInfo = {\n  id: string;\n  url: string;\n  title: string;\n  favicon: string | null;\n};\n"
  },
  {
    "path": "api/src/types/enums.ts",
    "content": "import { BrowserServerOptions } from \"./browser.js\";\n\nexport enum ScrapeFormat {\n  HTML = \"html\",\n  READABILITY = \"readability\",\n  CLEANED_HTML = \"cleaned_html\",\n  MARKDOWN = \"markdown\",\n}\n\nexport enum BrowserEventType {\n  Request = \"Request\",\n  Navigation = \"Navigation\",\n  Console = \"Console\",\n  PageError = \"PageError\",\n  RequestFailed = \"RequestFailed\",\n  Response = \"Response\",\n  Error = \"Error\",\n  BrowserError = \"BrowserError\",\n  Recording = \"Recording\",\n  ScreencastFrame = \"ScreencastFrame\",\n  CDPCommand = \"CDPCommand\",\n  CDPCommandResult = \"CDPCommandResult\",\n  CDPEvent = \"CDPEvent\",\n  ResponseBody = \"ResponseBody\",\n}\n\nexport enum EmitEvent {\n  Log = \"log\",\n  PageId = \"pageId\",\n  Recording = \"recording\",\n}\n"
  },
  {
    "path": "api/src/types/fastify.d.ts",
    "content": "import { FastifyRequest } from \"fastify\";\nimport { CDPService } from \"../services/cdp/cdp.service.js\";\nimport { SessionService } from \"../services/session.service.js\";\nimport { SeleniumService } from \"../services/selenium.service.js\";\nimport { Page } from \"puppeteer-core\";\nimport { FileService } from \"../services/file.service.js\";\n\ndeclare module \"fastify\" {\n  interface FastifyRequest {}\n  interface FastifyInstance {\n    seleniumService: SeleniumService;\n    sessionService: SessionService;\n    fileService: FileService;\n  }\n}\n"
  },
  {
    "path": "api/src/types/index.ts",
    "content": "export * from \"./enums.js\";\nexport * from \"./browser.js\";\nexport * from \"./websocket.js\";\n"
  },
  {
    "path": "api/src/types/turndown.d.ts",
    "content": "declare module \"@joplin/turndown\" {\n  export { default } from \"turndown\";\n  export { Options } from \"turndown\";\n  export { Node } from \"turndown\";\n}\n\ndeclare module \"@joplin/turndown-plugin-gfm\" {\n  export const gfm: any;\n  export const tables: any;\n  export const strikethrough: any;\n}\n"
  },
  {
    "path": "api/src/types/websocket.ts",
    "content": "import { IncomingMessage } from \"http\";\nimport { Duplex } from \"stream\";\nimport { WebSocketServer } from \"ws\";\nimport { FastifyInstance } from \"fastify\";\n\nexport interface WebSocketHandlerContext {\n  fastify: FastifyInstance;\n  wss: WebSocketServer;\n  params: Record<string, string>;\n}\n\nexport interface WebSocketHandler {\n  path: string;\n  handler: (\n    request: IncomingMessage,\n    socket: Duplex,\n    head: Buffer,\n    context: WebSocketHandlerContext,\n  ) => Promise<void> | void;\n}\n\nexport interface WebSocketHandlerRegistry {\n  handlers: Map<string, WebSocketHandler>;\n  registerHandler: (handler: WebSocketHandler) => void;\n  getHandler: (path: string) => WebSocketHandler | undefined;\n  matchHandler: (url: string) => WebSocketHandler | undefined;\n}\n"
  },
  {
    "path": "api/src/utils/browser.ts",
    "content": "import fs from \"fs\";\nimport path from \"path\";\nimport { Page } from \"puppeteer-core\";\nimport { env } from \"../env.js\";\n\nexport const getChromeExecutablePath = () => {\n  if (env.CHROME_EXECUTABLE_PATH) {\n    const executablePath = env.CHROME_EXECUTABLE_PATH;\n    const normalizedPath = path.normalize(executablePath);\n    if (!fs.existsSync(normalizedPath)) {\n      console.warn(`Your custom chrome executable at ${normalizedPath} does not exist`);\n    } else {\n      return executablePath;\n    }\n  }\n\n  if (process.platform === \"win32\") {\n    const programFilesPath = `${process.env[\"ProgramFiles\"]}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`;\n    const programFilesX86Path = `C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`;\n\n    if (fs.existsSync(programFilesPath)) {\n      return programFilesPath;\n    } else if (fs.existsSync(programFilesX86Path)) {\n      return programFilesX86Path;\n    }\n  }\n\n  if (process.platform === \"darwin\") {\n    return \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\";\n  }\n\n  return \"/usr/bin/chromium\";\n};\n\nexport async function installMouseHelper(page: Page, device: string) {\n  await page.evaluateOnNewDocument((deviceType) => {\n    // Install mouse helper only for top-level frame.\n    if (window !== window.parent) return;\n    window.addEventListener(\n      \"DOMContentLoaded\",\n      () => {\n        if (deviceType === \"desktop\") {\n          // Desktop mode: show regular arrow cursor\n          const CURSOR_ID = \"__cursor__\";\n          if (document.getElementById(CURSOR_ID)) return;\n\n          const cursor = document.createElement(\"div\");\n          cursor.id = CURSOR_ID;\n          Object.assign(cursor.style, {\n            position: \"fixed\",\n            top: \"0px\",\n            left: \"0px\",\n            width: \"20px\",\n            height: \"20px\",\n            backgroundImage: `url(\"data:image/svg+xml;utf8,<svg width='16' height='16' viewBox='0 0 20 20' fill='black' outline='white' xmlns='http://www.w3.org/2000/svg'><path d='M15.8089 7.22221C15.9333 7.00888 15.9911 6.78221 15.9822 6.54221C15.9733 6.29333 15.8978 6.06667 15.7555 5.86221C15.6133 5.66667 15.4311 5.52445 15.2089 5.43555L1.70222 0.0888888C1.47111 0 1.23555 -0.0222222 0.995555 0.0222222C0.746667 0.0755555 0.537779 0.186667 0.368888 0.355555C0.191111 0.533333 0.0755555 0.746667 0.0222222 0.995555C-0.0222222 1.23555 0 1.47111 0.0888888 1.70222L5.43555 15.2222C5.52445 15.4445 5.66667 15.6267 5.86221 15.7689C6.06667 15.9111 6.28888 15.9867 6.52888 15.9955H6.58221C6.82221 15.9955 7.04445 15.9333 7.24888 15.8089C7.44445 15.6845 7.59555 15.52 7.70221 15.3155L10.2089 10.2222L15.3022 7.70221C15.5155 7.59555 15.6845 7.43555 15.8089 7.22221Z' ></path></svg>\")`,\n            backgroundSize: \"cover\",\n            pointerEvents: \"none\",\n            zIndex: \"99999\",\n            transform: \"translate(-2px, -2px)\",\n          });\n\n          document.body.appendChild(cursor);\n\n          document.addEventListener(\"mousemove\", (e) => {\n            cursor.style.top = e.clientY + \"px\";\n            cursor.style.left = e.clientX + \"px\";\n          });\n        } else {\n          // Mobile mode: show circular touch indicator\n          const box = document.createElement(\"puppeteer-mouse-pointer\");\n          const styleElement = document.createElement(\"style\");\n          styleElement.innerHTML = `\n          puppeteer-mouse-pointer {\n            pointer-events: none;\n            position: absolute;\n            top: 0;\n            z-index: 10000;\n            left: 0;\n            width: 20px;\n            height: 20px;\n            background: rgba(0,0,0,.4);\n            border: 1px solid white;\n            border-radius: 10px;\n            margin: -10px 0 0 -10px;\n            padding: 0;\n            transition: background .2s, border-radius .2s, border-color .2s;\n          }\n          puppeteer-mouse-pointer.button-1 {\n            transition: none;\n            background: rgba(0,0,0,0.9);\n          }\n          puppeteer-mouse-pointer.button-2 {\n            transition: none;\n            border-color: rgba(0,0,255,0.9);\n          }\n          puppeteer-mouse-pointer.button-3 {\n            transition: none;\n            border-radius: 4px;\n          }\n          puppeteer-mouse-pointer.button-4 {\n            transition: none;\n            border-color: rgba(255,0,0,0.9);\n          }\n          puppeteer-mouse-pointer.button-5 {\n            transition: none;\n            border-color: rgba(0,255,0,0.9);\n          }\n        `;\n          document.head.appendChild(styleElement);\n          document.body.appendChild(box);\n          document.addEventListener(\n            \"mousemove\",\n            (event) => {\n              box.style.left = event.pageX + \"px\";\n              box.style.top = event.pageY + \"px\";\n              updateButtons(event.buttons);\n            },\n            true,\n          );\n          document.addEventListener(\n            \"mousedown\",\n            (event) => {\n              updateButtons(event.buttons);\n              box.classList.add(\"button-\" + event.which);\n            },\n            true,\n          );\n          document.addEventListener(\n            \"mouseup\",\n            (event) => {\n              updateButtons(event.buttons);\n              box.classList.remove(\"button-\" + event.which);\n            },\n            true,\n          );\n          function updateButtons(buttons) {\n            for (let i = 0; i < 5; i++)\n              // @ts-ignore\n              box.classList.toggle(\"button-\" + i, buttons & (1 << i));\n          }\n        }\n      },\n      false,\n    );\n  }, device);\n}\n\nexport function filterHeaders(headers: Record<string, string>) {\n  const headersToRemove = [\n    \"accept-encoding\",\n    \"accept\",\n    \"cache-control\",\n    \"pragma\",\n    \"sec-fetch-dest\",\n    \"sec-fetch-mode\",\n    \"sec-fetch-site\",\n    \"sec-fetch-user\",\n    \"upgrade-insecure-requests\",\n  ];\n  const filteredHeaders = { ...headers };\n  headersToRemove.forEach((header) => {\n    delete filteredHeaders[header];\n  });\n  return filteredHeaders;\n}\n"
  },
  {
    "path": "api/src/utils/casting.ts",
    "content": "import { Page } from \"puppeteer-core\";\nimport { NavigationEvent } from \"../types/casting.js\";\nimport { normalizeUrl } from \"./url.js\";\n\nexport const navigatePage = async (\n  event: NavigationEvent[\"event\"],\n  targetPage: Page,\n): Promise<void> => {\n  if (event.action === \"back\") {\n    await targetPage.goBack();\n  } else if (event.action === \"forward\") {\n    await targetPage.goForward();\n  } else if (event.action === \"refresh\") {\n    await targetPage.reload();\n  } else if (event.url) {\n    const formattedUrl = normalizeUrl(event.url) || event.url;\n    await targetPage.goto(formattedUrl);\n  }\n};\n\nexport const getPageTitle = async (page: Page): Promise<string> => {\n  try {\n    return await page.title();\n  } catch (error) {\n    return \"Untitled\";\n  }\n};\n\nexport const getPageFavicon = async (page: Page): Promise<string | null> => {\n  try {\n    return await page.evaluate(() => {\n      const iconLink = document.querySelector('link[rel=\"icon\"], link[rel=\"shortcut icon\"]');\n      if (iconLink) {\n        const href = iconLink.getAttribute(\"href\");\n        if (href?.startsWith(\"http\")) return href;\n        if (href?.startsWith(\"//\")) return window.location.protocol + href;\n        if (href?.startsWith(\"/\")) return window.location.origin + href;\n        return window.location.origin + \"/\" + href;\n      }\n      return null;\n    });\n  } catch (error) {\n    return null;\n  }\n};\n"
  },
  {
    "path": "api/src/utils/context.ts",
    "content": "import { Page } from \"puppeteer-core\";\nimport {\n  SessionData,\n  IndexedDBDatabase,\n  IndexedDBObjectStore,\n  IndexedDBRecord,\n  SessionStorageData,\n  LocalStorageData,\n} from \"../services/context/types.js\";\nimport { FastifyBaseLogger } from \"fastify\";\nimport { BrowserLauncherOptions } from \"../types/index.js\";\nimport path from \"path\";\n/**\n * Extract storage data for a single origin\n * @param client CDP session\n * @param origin Origin to process\n * @returns Storage data for the origin\n */\nexport async function extractStorageForPage(\n  page: Page,\n  logger: FastifyBaseLogger,\n): Promise<SessionData> {\n  const result: SessionData = {\n    localStorage: {},\n    sessionStorage: {},\n    indexedDB: {},\n  };\n\n  try {\n    // Skip pages that aren't valid or don't have a proper URL\n    const url = page.url();\n    if (!url || !url.startsWith(\"http\")) {\n      return result;\n    }\n\n    // Extract origin and domain from URL\n    const origin = new URL(url).origin;\n    const domain = new URL(url).hostname;\n\n    const client = await page.target().createCDPSession();\n\n    try {\n      // Check if the page has a valid main frame\n      const { frameTree } = await client\n        .send(\"Page.getFrameTree\")\n        .catch(() => ({ frameTree: null }));\n      if (!frameTree) {\n        logger.debug(`[CDPService] Page has no valid frame tree for ${domain}`);\n        return result;\n      }\n\n      // Get localStorage using CDP\n      try {\n        const localStorageResponse = await client.send(\"DOMStorage.getDOMStorageItems\", {\n          storageId: { securityOrigin: origin, isLocalStorage: true },\n        });\n\n        if (localStorageResponse?.entries?.length) {\n          result.localStorage![domain] = {};\n          for (const [key, value] of localStorageResponse.entries) {\n            result.localStorage![domain][key] = value;\n          }\n        }\n      } catch (err) {\n        // Lower log level to avoid flooding logs with expected errors\n        logger.trace(`[CDPService] Could not get localStorage for ${domain}: ${err}`);\n      }\n\n      // Get sessionStorage (note: only works for active pages)\n      try {\n        const sessionStorageResponse = await client.send(\"DOMStorage.getDOMStorageItems\", {\n          storageId: { securityOrigin: origin, isLocalStorage: false },\n        });\n\n        if (sessionStorageResponse?.entries?.length) {\n          result.sessionStorage![domain] = {};\n          for (const [key, value] of sessionStorageResponse.entries) {\n            result.sessionStorage![domain][key] = value;\n          }\n        }\n      } catch (err) {\n        // Lower log level to avoid flooding logs with expected errors\n        logger.trace(`[CDPService] Could not get sessionStorage for ${domain}: ${err}`);\n      }\n\n      // Get IndexedDB databases\n      try {\n        const dbResponse = await client.send(\"IndexedDB.requestDatabaseNames\", {\n          securityOrigin: origin,\n        });\n\n        const databaseNames = dbResponse?.databaseNames || [];\n\n        if (databaseNames.length) {\n          result.indexedDB![domain] = [];\n\n          // Process each database\n          for (let dbIndex = 0; dbIndex < databaseNames.length; dbIndex++) {\n            const dbName = databaseNames[dbIndex];\n\n            // Create a properly structured database object\n            const database: IndexedDBDatabase = {\n              id: dbIndex,\n              name: dbName,\n              data: [],\n            };\n\n            // Get database schema\n            const dbSchemaResponse = await client.send(\"IndexedDB.requestDatabase\", {\n              securityOrigin: origin,\n              databaseName: dbName,\n            });\n\n            // Access object stores safely\n            const objectStores = dbSchemaResponse?.databaseWithObjectStores?.objectStores || [];\n\n            // Process each object store\n            for (let storeIndex = 0; storeIndex < objectStores.length; storeIndex++) {\n              const store = objectStores[storeIndex];\n\n              // Create a properly structured object store\n              const objectStore: IndexedDBObjectStore = {\n                id: storeIndex,\n                name: store.name,\n                records: [],\n              };\n\n              // Paginate through all records\n              let hasMoreData = true;\n              let skipCount = 0;\n              const pageSize = 1000;\n\n              while (hasMoreData) {\n                const dataResponse = await client.send(\"IndexedDB.requestData\", {\n                  securityOrigin: origin,\n                  databaseName: dbName,\n                  objectStoreName: store.name,\n                  indexName: \"\", // Empty string means use primary key\n                  skipCount,\n                  pageSize,\n                });\n\n                // Add the retrieved data\n                const objectStoreData = dataResponse?.objectStoreDataEntries || [];\n                if (objectStoreData.length) {\n                  // Map the data to the correct record format\n                  const records: IndexedDBRecord[] = objectStoreData.map((entry) => ({\n                    key: entry.key,\n                    value: entry.value,\n                    // TODO: Add blob files\n                  }));\n\n                  objectStore.records.push(...records);\n                }\n\n                // Check if we need to continue pagination\n                hasMoreData = !!dataResponse?.hasMore;\n                skipCount += objectStoreData.length;\n\n                // Safety check to prevent infinite loops\n                if (objectStoreData.length === 0) break;\n              }\n\n              // Add the object store to the database\n              database.data.push(objectStore);\n            }\n\n            // Add the database to the result\n            result.indexedDB![domain].push(database);\n          }\n        }\n      } catch (err) {\n        // Lower log level to avoid flooding logs with expected errors\n        logger.trace(`[CDPService] Could not get IndexedDB for ${domain}: ${err}`);\n      }\n    } finally {\n      // Always ensure the client session is detached\n      await client.detach().catch(() => {});\n    }\n  } catch (err) {\n    logger.warn(`[CDPService] Error extracting storage for page: ${err}`);\n  }\n\n  return result;\n}\n\n// Create our frameNavigated handler\nexport const handleFrameNavigated = async (\n  frame: any,\n  storageByOrigin: Map<\n    string,\n    {\n      localStorage?: LocalStorageData;\n      sessionStorage?: SessionStorageData;\n      indexedDB?: IndexedDBDatabase[];\n    }\n  >,\n  logger: FastifyBaseLogger,\n) => {\n  // Only process top-level frames\n  if (frame.parentFrame()) return;\n\n  try {\n    const url = frame.url();\n    if (!url || !url.startsWith(\"http\")) return;\n\n    // Extract the origin from the URL\n    const origin = new URL(url).origin;\n\n    // Check if we have storage for this origin\n    const storage = storageByOrigin.get(origin);\n    if (!storage) return;\n\n    logger.debug(`[CDPService] Injecting storage for navigated origin: ${origin}`);\n\n    // Set localStorage if available\n    if (storage.localStorage) {\n      await frame.evaluate((items) => {\n        for (const [key, value] of Object.entries(items)) {\n          try {\n            if (typeof value === \"string\") {\n              localStorage.setItem(key, value);\n            }\n          } catch (e) {\n            console.error(`Error setting localStorage: ${e}`);\n          }\n        }\n      }, storage.localStorage);\n    }\n\n    // Set sessionStorage if available\n    if (storage.sessionStorage) {\n      await frame.evaluate((items) => {\n        for (const [key, value] of Object.entries(items)) {\n          try {\n            if (typeof value === \"string\") {\n              sessionStorage.setItem(key, value);\n            }\n          } catch (e) {\n            console.error(`Error setting sessionStorage: ${e}`);\n          }\n        }\n      }, storage.sessionStorage);\n    }\n\n    // Set IndexedDB if available\n    if (storage.indexedDB && storage.indexedDB.length > 0) {\n      for (const database of storage.indexedDB) {\n        if (!database.name || !database.data) continue;\n\n        // Create a store map for this database\n        const storeMap = {};\n\n        for (const store of database.data) {\n          if (!store.name || !store.records || store.records.length === 0) continue;\n\n          storeMap[store.name] = store.records.map((record) => {\n            try {\n              // Parse the key and value if they're stored as strings\n              const parsedKey =\n                typeof record.key === \"string\" ? JSON.parse(record.key) : record.key;\n              const parsedValue =\n                typeof record.value === \"string\" ? JSON.parse(record.value) : record.value;\n              return { key: parsedKey, value: parsedValue };\n            } catch (e) {\n              // Fall back to original values if parsing fails\n              return { key: record.key, value: record.value };\n            }\n          });\n        }\n\n        if (Object.keys(storeMap).length === 0) continue;\n\n        await frame.evaluate(\n          async (dbName, stores) => {\n            return new Promise((resolve, reject) => {\n              try {\n                const openRequest = indexedDB.open(dbName, 1);\n\n                openRequest.onupgradeneeded = function (event) {\n                  const db = (event.target as IDBOpenDBRequest).result;\n\n                  // Create object stores from our data\n                  for (const storeName of Object.keys(stores)) {\n                    if (!db.objectStoreNames.contains(storeName)) {\n                      db.createObjectStore(storeName, { keyPath: \"key\" });\n                    }\n                  }\n                };\n\n                openRequest.onsuccess = function (event) {\n                  const db = (event.target as IDBOpenDBRequest).result;\n                  let completedStores = 0;\n                  const totalStores = Object.keys(stores).length;\n\n                  for (const [storeName, storeData] of Object.entries(stores)) {\n                    if (!db.objectStoreNames.contains(storeName)) {\n                      // Skip if object store doesn't exist and can't be created\n                      completedStores++;\n                      continue;\n                    }\n\n                    const transaction = db.transaction(storeName, \"readwrite\");\n                    const objectStore = transaction.objectStore(storeName);\n\n                    // Add all items\n                    for (const item of storeData as any[]) {\n                      try {\n                        objectStore.put(item);\n                      } catch (e) {\n                        console.error(`Error adding item to IndexedDB: ${e}`);\n                      }\n                    }\n\n                    transaction.oncomplete = function () {\n                      completedStores++;\n                      if (completedStores === totalStores) {\n                        resolve(true);\n                      }\n                    };\n\n                    transaction.onerror = function (err) {\n                      console.error(`Transaction error: ${err}`);\n                      completedStores++;\n                      if (completedStores === totalStores) {\n                        resolve(false);\n                      }\n                    };\n                  }\n\n                  // Handle case with no stores\n                  if (totalStores === 0) {\n                    resolve(true);\n                  }\n                };\n\n                openRequest.onerror = function (event) {\n                  reject(`Error opening IndexedDB: ${(event.target as IDBOpenDBRequest).error}`);\n                };\n              } catch (e) {\n                reject(`IndexedDB restore error: ${e}`);\n              }\n            });\n          },\n          database.name,\n          storeMap,\n        );\n      }\n    }\n  } catch (err) {\n    logger.error(`[CDPService] Error injecting storage during navigation: ${err}`);\n  }\n};\n\n/**\n * Organizes session storage data by origin for efficient lookup\n * @param context Session context data from BrowserLauncherOptions\n * @returns Map of origins to their storage data\n */\nexport function groupSessionStorageByOrigin(\n  context?: BrowserLauncherOptions[\"sessionContext\"],\n): Map<\n  string,\n  {\n    localStorage?: LocalStorageData;\n    sessionStorage?: SessionStorageData;\n    indexedDB?: IndexedDBDatabase[];\n  }\n> {\n  const result = new Map<\n    string,\n    {\n      localStorage?: LocalStorageData;\n      sessionStorage?: SessionStorageData;\n      indexedDB?: IndexedDBDatabase[];\n    }\n  >();\n\n  if (!context) return result;\n\n  if (context.localStorage) {\n    for (const [domain, storage] of Object.entries(context.localStorage)) {\n      if (!result.has(domain)) {\n        result.set(domain, {});\n      }\n      result.get(domain)!.localStorage = storage;\n    }\n  }\n\n  if (context.sessionStorage) {\n    for (const [domain, storage] of Object.entries(context.sessionStorage)) {\n      if (!result.has(domain)) {\n        result.set(domain, {});\n      }\n      result.get(domain)!.sessionStorage = storage;\n    }\n  }\n\n  if (context.indexedDB) {\n    for (const [domain, databases] of Object.entries(context.indexedDB)) {\n      if (!result.has(domain)) {\n        result.set(domain, {});\n      }\n      result.get(domain)!.indexedDB = databases;\n    }\n  }\n\n  return result;\n}\n\n/**\n * Helper to get Chrome profile paths in a cross-platform way\n * Takes into account different Chrome profile directory structures\n */\nexport function getProfilePath(userDataDir: string, ...pathSegments: string[]): string {\n  // Chrome profile directories vary by platform and version\n  // Both \"Default\" and \"Profile 1\" are standard locations\n  const possibleProfileDirs = [\"Default\", \"Profile 1\"];\n\n  // First check if the userDataDir already includes a profile directory\n  const dirName = path.basename(userDataDir);\n  if (possibleProfileDirs.includes(dirName)) {\n    // userDataDir already points to a profile directory\n    return path.join(userDataDir, ...pathSegments);\n  }\n\n  const defaultPath = path.join(userDataDir, \"Default\", ...pathSegments);\n\n  return defaultPath;\n}\n\n/**\n * Deep merge two objects, with the second object taking precedence.\n * Arrays are replaced entirely, not merged.\n */\nexport function deepMerge<T = any>(target: T, source: Partial<T>): T {\n  if (typeof target !== \"object\" || target === null) {\n    return source as T;\n  }\n  if (typeof source !== \"object\" || source === null) {\n    return target;\n  }\n\n  const result = { ...target };\n\n  for (const key in source) {\n    if (source.hasOwnProperty(key)) {\n      const sourceValue = source[key];\n      const targetValue = (result as any)[key];\n\n      if (typeof sourceValue === \"object\" && sourceValue !== null && !Array.isArray(sourceValue)) {\n        (result as any)[key] = deepMerge(targetValue, sourceValue);\n      } else {\n        (result as any)[key] = sourceValue;\n      }\n    }\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "api/src/utils/errors.ts",
    "content": "export function getErrors(e: unknown) {\n  let error: string;\n  if (typeof e === \"string\") {\n    error = e;\n  } else if (e instanceof Error) {\n    error = e.message;\n  } else {\n    error = \"Unknown error\";\n  }\n\n  return error;\n}\n"
  },
  {
    "path": "api/src/utils/extensions.ts",
    "content": "import fs from \"fs\";\nimport path, { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\nexport async function getExtensionPaths(extensionNames: string[]): Promise<string[]> {\n  const extensionsDir = path.join(\n    dirname(fileURLToPath(import.meta.url)),\n    \"..\",\n    \"..\",\n    \"extensions\",\n  );\n\n  try {\n    await fs.promises.access(extensionsDir);\n  } catch {\n    console.warn(\"Extensions directory does not exist\");\n    return [];\n  }\n\n  const allExtensions = await fs.promises.readdir(extensionsDir);\n\n  const candidatePaths = extensionNames\n    .filter((name) => allExtensions.includes(name))\n    .map((dir) => path.join(extensionsDir, dir));\n\n  const validationResults = await Promise.all(\n    candidatePaths.map(async (fullPath) => {\n      try {\n        await fs.promises.access(fullPath);\n        return { path: fullPath, valid: true };\n      } catch {\n        console.warn(`Extension directory ${fullPath} does not exist`);\n        return { path: fullPath, valid: false };\n      }\n    }),\n  );\n\n  return validationResults.filter((result) => result.valid).map((result) => result.path);\n}\n"
  },
  {
    "path": "api/src/utils/leveldb.ts",
    "content": "import path from \"path\";\nimport fs from \"fs/promises\";\n\n/**\n * Utility to copy a LevelDB directory to a temporary path if opening directly fails (e.g. database lock).\n */\nexport async function copyDirectory(src: string, dest: string): Promise<void> {\n  await fs.mkdir(dest, { recursive: true });\n  const entries = await fs.readdir(src, { withFileTypes: true });\n  await Promise.all(\n    entries.map(async (entry) => {\n      const srcPath = path.join(src, entry.name);\n      const destPath = path.join(dest, entry.name);\n      if (entry.isDirectory()) {\n        await copyDirectory(srcPath, destPath);\n      } else if (entry.isFile()) {\n        const data = await fs.readFile(srcPath);\n        await fs.writeFile(destPath, data);\n      }\n    }),\n  );\n}\n"
  },
  {
    "path": "api/src/utils/logging.ts",
    "content": "export const updateLog = async (logUrl: string, log: any) => {\n  try {\n    const response = await fetch(logUrl, {\n      method: \"PUT\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify(log),\n    });\n    if (!response.ok) {\n      const error = await response.text();\n      console.error(\"Failed to update log\", error);\n    }\n  } catch (e: unknown) {\n    console.error(e);\n  }\n};\n"
  },
  {
    "path": "api/src/utils/passthough-proxy.ts",
    "content": "import http, { IncomingHttpHeaders } from \"node:http\";\nimport { PrepareRequestFunctionOpts, PrepareRequestFunctionResult } from \"proxy-chain\";\n\n// Headers that are hop-by-hop and should not be forwarded RFC 2616, Section 13.5.1\nexport const hopByHopHeaders = new Set([\n  \"connection\",\n  \"proxy-authenticate\",\n  \"proxy-authorization\",\n  \"keep-alive\",\n  \"te\",\n  \"trailers\",\n  \"transfer-encoding\",\n  \"upgrade\",\n]);\n\n/**\n * Creates a simple http proxy which reverse proxies the original request to the original host.\n * There's an issue with proxy-chain's implementation causing corruption in our internals requests\n */\nexport const PassthroughServer = http.createServer((clientReq, clientRes) => {\n  const targetUrl = new URL(clientReq.url ?? \"/\", `http://${clientReq.headers.host}`);\n\n  const proxyHeaders: IncomingHttpHeaders = {};\n  for (const [key, value] of Object.entries(clientReq.headers)) {\n    const lowerKey = key.toLowerCase();\n    if (!hopByHopHeaders.has(lowerKey)) {\n      proxyHeaders[lowerKey] = value;\n    }\n  }\n\n  proxyHeaders[\"host\"] = targetUrl.host;\n  proxyHeaders[\"x-forwarded-for\"] = clientReq.socket.remoteAddress || \"\";\n  proxyHeaders[\"x-forwarded-proto\"] = \"http\";\n  proxyHeaders[\"x-forwarded-host\"] = clientReq.headers.host || \"\";\n\n  const proxyReq = http.request(\n    {\n      method: clientReq.method,\n      headers: proxyHeaders,\n      hostname: targetUrl.hostname,\n      port: targetUrl.port,\n      path: targetUrl.pathname + targetUrl.search,\n    },\n    (proxyRes) => {\n      const clientResHeaders = {};\n      for (const [key, value] of Object.entries(proxyRes.headers)) {\n        if (!hopByHopHeaders.has(key.toLowerCase())) {\n          clientResHeaders[key] = value;\n        }\n      }\n\n      clientRes.writeHead(proxyRes.statusCode ?? 500, clientResHeaders);\n      proxyRes.pipe(clientRes);\n    },\n  );\n\n  proxyReq.on(\"error\", (err) => {\n    console.error(`Proxy error for ${targetUrl}:`, err);\n    if (!clientRes.headersSent) {\n      clientRes.writeHead(502);\n    }\n    clientRes.end(\"Proxy error: Could not connect to the target service.\");\n  });\n\n  clientReq.on(\"error\", (err) => {\n    console.error(`Client request error:`, err);\n    proxyReq.destroy();\n  });\n\n  clientReq.pipe(proxyReq);\n});\n\ntype Result<T> = [err: Error, result: null] | [err: null, result: T];\n\n/**\n * There's an issue with proxy-chain's handling of chunked requests when doing a direct passthrough.\n * This workaround forwards the requests manually and returns the response\n */\nexport const makePassthrough = function ({\n  request,\n  hostname,\n  port,\n}: PrepareRequestFunctionOpts): NonNullable<\n  PrepareRequestFunctionResult[\"customResponseFunction\"]\n> {\n  return async () => {\n    const [err, proxyRes]: Result<http.IncomingMessage> = await new Promise((resolve) => {\n      const forward = http.request(\n        {\n          hostname,\n          port,\n          method: request.method,\n          path: request.url,\n          headers: request.headers,\n        },\n        (res) => resolve([null, res]),\n      );\n\n      forward.on(\"error\", (err) => resolve([err, null]));\n      request.pipe(forward);\n    });\n\n    if (err) {\n      console.error(`Request failed \"${err.name}\": ${err.message}`);\n      throw err;\n    }\n\n    const chunks: Buffer[] = [];\n    for await (const chunk of proxyRes) chunks.push(chunk);\n    const body = Buffer.concat(chunks);\n\n    const headers: IncomingHttpHeaders = {};\n    for (const [k, v] of Object.entries(proxyRes.headers)) {\n      if (!hopByHopHeaders.has(k.toLowerCase()) && v !== undefined) {\n        headers[k] = Array.isArray(v) ? v.join(\",\") : v;\n      }\n    }\n\n    return {\n      statusCode: proxyRes.statusCode ?? 500,\n      headers: proxyRes.headers as Record<string, string>,\n      body,\n    };\n  };\n};\n"
  },
  {
    "path": "api/src/utils/proxy.ts",
    "content": "import { env } from \"../env.js\";\nimport { SessionService } from \"../services/session.service.js\";\nimport { makePassthrough, PassthroughServer } from \"./passthough-proxy.js\";\nimport { Server } from \"proxy-chain\";\n\nexport interface IProxyServer {\n  readonly url: string;\n  readonly upstreamProxyUrl: string;\n  readonly txBytes: number;\n  readonly rxBytes: number;\n  listen(): Promise<void>;\n  close(force?: boolean): Promise<void>;\n}\n\nexport class ProxyServer extends Server implements IProxyServer {\n  public url: string;\n  public upstreamProxyUrl: string;\n  public txBytes = 0;\n  public rxBytes = 0;\n  private hostConnections = new Set<number>();\n\n  constructor(proxyUrl: string) {\n    super({\n      port: 0,\n\n      prepareRequestFunction: (options) => {\n        const { connectionId, hostname } = options;\n\n        const internalBypassTests = new Set([\"0.0.0.0\", process.env.HOST]);\n\n        if (env.PROXY_INTERNAL_BYPASS) {\n          for (const host of env.PROXY_INTERNAL_BYPASS.split(\",\")) {\n            internalBypassTests.add(host.trim());\n          }\n        }\n\n        const isInternalBypass = internalBypassTests.has(hostname);\n\n        if (isInternalBypass) {\n          this.hostConnections.add(connectionId);\n          return {\n            customConnectServer: PassthroughServer,\n            customResponseFunction: makePassthrough(options),\n          };\n        }\n        return {\n          requestAuthentication: false,\n          upstreamProxyUrl: proxyUrl,\n        };\n      },\n    });\n\n    this.on(\"connectionClosed\", ({ connectionId, stats }) => {\n      if (stats && !this.hostConnections.has(connectionId)) {\n        this.txBytes += stats.trgTxBytes;\n        this.rxBytes += stats.trgRxBytes;\n      }\n      this.hostConnections.delete(connectionId);\n    });\n\n    this.url = `http://127.0.0.1:${this.port}`;\n    this.upstreamProxyUrl = proxyUrl;\n  }\n\n  async listen(): Promise<void> {\n    await super.listen();\n    this.url = `http://127.0.0.1:${this.port}`;\n  }\n}\n"
  },
  {
    "path": "api/src/utils/requests.ts",
    "content": "const AD_HOSTS = [\n  // Ad Networks & Services\n  \"doubleclick.net\",\n  \"adservice.google.com\",\n  \"googlesyndication.com\",\n  \"google-analytics.com\",\n  \"adnxs.com\",\n  \"rubiconproject.com\",\n  \"advertising.com\",\n  \"adtechus.com\",\n  \"quantserve.com\",\n  \"scorecardresearch.com\",\n  \"casalemedia.com\",\n  \"moatads.com\",\n  \"criteo.com\",\n  \"amazon-adsystem.com\",\n  \"serving-sys.com\",\n  \"adroll.com\",\n  \"chartbeat.com\",\n  \"sharethrough.com\",\n  \"indexww.com\",\n  \"mediamath.com\",\n  \"adsystem.com\",\n  \"adservice.com\",\n  \"adnxs.com\",\n  \"ads-twitter.com\",\n\n  // Analytics & Tracking\n  \"hotjar.com\",\n  \"analytics.google.com\",\n  \"mixpanel.com\",\n  \"kissmetrics.com\",\n  \"googletagmanager.com\",\n  // Microsoft Clarity\n  \"clarity.ms\",\n  \"www.clarity.ms\",\n  \"static.clarity.ms\",\n\n  // Ad Exchanges\n  \"openx.net\",\n  \"pubmatic.com\",\n  \"bidswitch.net\",\n  \"taboola.com\",\n  \"outbrain.com\",\n\n  // Social Media Tracking\n  \"facebook.com/tr/\",\n  \"connect.facebook.net\",\n  \"platform.twitter.com\",\n  \"ads.linkedin.com\",\n];\n\nconst RE_IMAGE_EXT = /\\.(jpg|jpeg|png|webp|svg|ico)(\\?.*)?$/i;\nconst RE_VIDEO_EXT = /\\.(mp4|m4s|m3u8|ts|webm|gif)(\\?.*)?$/i;\nconst RE_RANGE = /range=\\d+-\\d+/i;\n\nexport function tryParseUrl(url: string): URL | null {\n  try {\n    return new URL(url);\n  } catch {\n    return null;\n  }\n}\n\nexport function isAdRequest(parsed: URL): boolean {\n  const { hostname } = parsed;\n  return AD_HOSTS.some((adHost) => hostname === adHost || hostname.endsWith(`.${adHost}`));\n}\n\nexport function isImageRequest(parsed: URL): boolean {\n  return RE_IMAGE_EXT.test(parsed.pathname);\n}\n\nexport function isHeavyMediaRequest(parsed: URL): boolean {\n  const { pathname, searchParams } = parsed;\n  if (RE_VIDEO_EXT.test(pathname)) return true;\n  const isRange = searchParams.has(\"range\") || RE_RANGE.test(parsed.href);\n  return isRange && pathname.includes(\"/avf/\");\n}\n\nexport function isHostBlocked(parsed: URL, blockedHosts?: string[]): boolean {\n  if (!blockedHosts?.length) return false;\n  const { hostname } = parsed;\n  return blockedHosts.some((h) => hostname === h || hostname.endsWith(`.${h}`));\n}\n\nexport function compileUrlPatterns(patterns: string[]): RegExp[] {\n  return patterns.map((pattern) => {\n    try {\n      return new RegExp(\n        `^${pattern.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\")}$`,\n        \"i\",\n      );\n    } catch {\n      // Fallback: escape the entire pattern for literal matching\n      return new RegExp(pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"i\");\n    }\n  });\n}\n\nexport function isUrlMatchingPatterns(url: string, compiledPatterns?: RegExp[]): boolean {\n  if (!compiledPatterns?.length) return false;\n  return compiledPatterns.some((re) => re.test(url));\n}\n"
  },
  {
    "path": "api/src/utils/retry.ts",
    "content": "import { FastifyBaseLogger } from \"fastify\";\nimport {\n  BaseLaunchError,\n  ConfigurationError,\n  LaunchTimeoutError,\n  ResourceError,\n} from \"../services/cdp/errors/launch-errors.js\";\n\nexport interface RetryOptions {\n  maxAttempts: number;\n  baseDelayMs: number;\n  maxDelayMs: number;\n  backoffMultiplier: number;\n  jitterMs?: number;\n}\n\nexport interface RetryResult<T> {\n  result: T;\n  attempt: number;\n  totalDuration: number;\n}\n\nexport class RetryError extends Error {\n  public readonly attempts: number;\n  public readonly lastError: Error;\n  public readonly allErrors: Error[];\n\n  constructor(attempts: number, lastError: Error, allErrors: Error[]) {\n    super(`Failed after ${attempts} attempts. Last error: ${lastError.message}`);\n    this.name = \"RetryError\";\n    this.attempts = attempts;\n    this.lastError = lastError;\n    this.allErrors = allErrors;\n  }\n}\n\n/**\n * Retry utility with exponential backoff and jitter for retryable launch errors\n */\nexport class RetryManager {\n  private logger: FastifyBaseLogger;\n  private defaultOptions: RetryOptions = {\n    maxAttempts: 3,\n    baseDelayMs: 500,\n    maxDelayMs: 5000,\n    backoffMultiplier: 2,\n    jitterMs: 250,\n  };\n\n  constructor(logger: FastifyBaseLogger) {\n    this.logger = logger;\n  }\n\n  /**\n   * Execute a function with retry logic for retryable errors\n   */\n  async executeWithRetry<T>(\n    operation: () => Promise<T>,\n    operationName: string,\n    options: Partial<RetryOptions> = {},\n  ): Promise<RetryResult<T>> {\n    const opts = { ...this.defaultOptions, ...options };\n    const errors: Error[] = [];\n    const startTime = Date.now();\n\n    for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {\n      try {\n        this.logger.info(\n          `[RetryManager] ${operationName} - Attempt ${attempt}/${opts.maxAttempts}`,\n        );\n\n        const result = await operation();\n        const totalDuration = Date.now() - startTime;\n\n        if (attempt > 1) {\n          this.logger.info(\n            `[RetryManager] ${operationName} succeeded on attempt ${attempt}/${opts.maxAttempts} after ${totalDuration}ms`,\n          );\n        }\n\n        return {\n          result,\n          attempt,\n          totalDuration,\n        };\n      } catch (error) {\n        const err = error instanceof Error ? error : new Error(String(error));\n        errors.push(err);\n\n        const isRetryable = this.isErrorRetryable(err);\n        const isLastAttempt = attempt === opts.maxAttempts;\n\n        this.logger.warn(\n          {\n            error: err.message,\n            isRetryable,\n            isLastAttempt,\n            errorType: err instanceof BaseLaunchError ? err.type : \"unknown\",\n          },\n          `[RetryManager] ${operationName} failed on attempt ${attempt}/${opts.maxAttempts}`,\n        );\n\n        if (!isRetryable || isLastAttempt) {\n          if (!isRetryable) {\n            this.logger.error(\n              `[RetryManager] ${operationName} failed with non-retryable error: ${err.message}`,\n            );\n            throw err; // Throw original error for non-retryable errors\n          } else {\n            this.logger.error(\n              `[RetryManager] ${operationName} failed after ${opts.maxAttempts} attempts`,\n            );\n            throw new RetryError(attempt, err, errors);\n          }\n        }\n\n        // Calculate delay with exponential backoff and jitter\n        const baseDelay = opts.baseDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1);\n        const jitter = opts.jitterMs ? Math.random() * opts.jitterMs : 0;\n        const delay = Math.min(baseDelay + jitter, opts.maxDelayMs);\n\n        this.logger.info(\n          `[RetryManager] Waiting ${Math.round(delay)}ms before retry ${attempt + 1}/${\n            opts.maxAttempts\n          }`,\n        );\n        await this.sleep(delay);\n      }\n    }\n\n    // This should never be reached, but TypeScript needs it\n    throw new RetryError(opts.maxAttempts, errors[errors.length - 1], errors);\n  }\n\n  private isErrorRetryable(error: Error): boolean {\n    if (\n      error instanceof ConfigurationError ||\n      error instanceof ResourceError ||\n      error instanceof LaunchTimeoutError\n    ) {\n      return false;\n    }\n\n    if (error instanceof BaseLaunchError) {\n      return error.isRetryable;\n    }\n\n    // For non-categorized errors, we'll be conservative and not retry.\n    return false;\n  }\n\n  private sleep(ms: number): Promise<void> {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n\n  createRetryWrapper<T extends any[], R>(\n    method: (...args: T) => Promise<R>,\n    operationName: string,\n    options: Partial<RetryOptions> = {},\n  ): (...args: T) => Promise<R> {\n    return async (...args: T): Promise<R> => {\n      const result = await this.executeWithRetry(() => method(...args), operationName, options);\n      return result.result;\n    };\n  }\n}\n"
  },
  {
    "path": "api/src/utils/schema.ts",
    "content": "import { ZodType, z } from \"zod\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\n\nexport type Models<Key extends string = string> = {\n  readonly [K in Key]: ZodType<unknown>;\n};\n\nexport type BuildJsonSchemasOptions = {\n  readonly $id?: string;\n  readonly target?: `jsonSchema7` | `openApi3`;\n  readonly errorMessages?: boolean;\n};\n\nexport type SchemaKey<M extends Models> = M extends Models<infer Key> ? Key & string : never;\n\nexport type SchemaKeyOrDescription<M extends Models> =\n  | SchemaKey<M>\n  | {\n      readonly description: string;\n      readonly key: SchemaKey<M>;\n    };\n\nexport type $Ref<M extends Models> = (key: SchemaKeyOrDescription<M>) => {\n  readonly $ref: string;\n  readonly description?: string;\n};\n\nexport type JsonSchema = {\n  readonly $id: string;\n};\n\nexport type BuildJsonSchemasResult<M extends Models> = {\n  readonly schemas: JsonSchema[];\n  readonly $ref: $Ref<M>;\n};\n\nexport const buildJsonSchemas = <M extends Models>(\n  models: M,\n  opts: BuildJsonSchemasOptions = {},\n): BuildJsonSchemasResult<M> => {\n  const zodSchema = z.object(models);\n\n  const zodJsonSchema = zodToJsonSchema(zodSchema, {\n    target: \"openApi3\",\n    $refStrategy: \"none\",\n    errorMessages: opts.errorMessages,\n  });\n\n  const cleanedSchemas = Object.entries(\n    //@ts-ignore\n    zodJsonSchema.properties as { [key: string]: any },\n  ).reduce((acc, [key, value]) => {\n    return [...acc, { $id: key, title: key, ...value }];\n  }, [] as JsonSchema[]);\n\n  const $ref: $Ref<M> = (key) => {\n    const $ref = `${typeof key === `string` ? key : key.key}#`;\n    return typeof key === `string`\n      ? {\n          $ref,\n        }\n      : {\n          $ref,\n          description: key.description,\n        };\n  };\n  return {\n    schemas: cleanedSchemas,\n    $ref,\n  };\n};\n"
  },
  {
    "path": "api/src/utils/scrape/cleanHtml.ts",
    "content": "import { JSDOM } from \"jsdom\";\n\nexport const cleanHtml = (html: string): string => {\n  const blacklistedElements = new Set([\n    \"head\",\n    \"title\",\n    \"meta\",\n    \"script\",\n    \"style\",\n    \"path\",\n    \"svg\",\n    \"br\",\n    \"hr\",\n    \"link\",\n    \"object\",\n    \"embed\",\n  ]);\n\n  const blacklistedAttributes = [\n    \"style\",\n    \"ping\",\n    \"src\",\n    \"item.*\",\n    \"aria.*\",\n    \"js.*\",\n    \"data-.*\",\n    \"role\",\n    \"tabindex\",\n    \"onerror\",\n  ];\n\n  const dom = new JSDOM(html);\n  const document = dom.window.document;\n\n  // Remove blacklisted elements\n  blacklistedElements.forEach((tag) => {\n    const elements = document.querySelectorAll(tag);\n    elements.forEach((element) => {\n      element.remove();\n    });\n  });\n\n  // Remove blacklisted attributes\n  const elements = document.querySelectorAll(\"*\");\n  elements.forEach((element) => {\n    blacklistedAttributes.forEach((attrPattern) => {\n      const regex = new RegExp(`^${attrPattern}$`);\n      Array.from(element.attributes).forEach((attr: any) => {\n        if (regex.test(attr.name)) {\n          element.removeAttribute(attr.name);\n        }\n      });\n    });\n  });\n\n  // Remove empty elements\n  elements.forEach((element) => {\n    if (!element.hasAttributes() && element.textContent?.trim() === \"\") {\n      element.remove();\n    }\n  });\n\n  const sourceCode = document.documentElement.outerHTML;\n\n  return sourceCode;\n};\n"
  },
  {
    "path": "api/src/utils/scrape/htmlToMarkdown.ts",
    "content": "import { applyFixes } from \"markdownlint\";\nimport { lint } from \"markdownlint/promise\";\nimport Turndown from \"turndown\";\nimport highlightedCodeBlock from \"./plugins/highlightedCodeBlock.js\";\nimport inlineLink from \"./plugins/inlineLink.js\";\nimport strikethrough from \"./plugins/strikethrough.js\";\nimport tables from \"./plugins/table.js\";\nimport taskListItems from \"./plugins/taskListItems.js\";\n\nconst turndownService = new Turndown({\n  headingStyle: \"atx\",\n  codeBlockStyle: \"fenced\",\n  bulletListMarker: \"-\",\n  emDelimiter: \"*\",\n  strongDelimiter: \"**\",\n  linkStyle: \"inlined\",\n  preformattedCode: false,\n}).use([highlightedCodeBlock, strikethrough, taskListItems, inlineLink, tables]);\n\nexport const htmlToMarkdown = async (html: string): Promise<string> => {\n  let markdown: string;\n\n  markdown = turndownService.turndown(html).trim();\n  markdown = newlinesToSpacesInLinks(markdown);\n  markdown = await lintMarkdown(markdown);\n\n  return markdown;\n};\n\nconst lintMarkdown = async (md: string) => {\n  const lintResult = await lint({\n    strings: { md },\n    config: {\n      \"no-trailing-punctuation\": false,\n    },\n    resultVersion: 3,\n  });\n  const fixes = lintResult[\"md\"].filter((error) => error.fixInfo);\n\n  if (fixes.length > 0) {\n    return applyFixes(md, fixes).trim();\n  }\n\n  return md.trim();\n};\n\nconst newlinesToSpacesInLinks = (markdownContent: string) => {\n  const linkRegex = /\\[([\\s\\S]*?)\\]\\(([\\s\\S]*?)\\)/g;\n\n  return markdownContent.replace(linkRegex, (_match, linkText, linkUrl) => {\n    const cleanedText = linkText.trim().replace(/\\s+/g, \" \");\n    const cleanedUrl = linkUrl.trim().replace(/\\s+/g, \"\");\n\n    return `[${cleanedText}](${cleanedUrl})`;\n  });\n};\n"
  },
  {
    "path": "api/src/utils/scrape/index.ts",
    "content": "export { cleanHtml } from \"./cleanHtml.js\";\nexport { htmlToMarkdown } from \"./htmlToMarkdown.js\";\nexport { getDefuddleContent } from \"./readability.js\";\nexport { transformHtml } from \"./transformHtml.js\";\n"
  },
  {
    "path": "api/src/utils/scrape/pdfToHtml.ts",
    "content": "import { load as loadHtml } from \"cheerio\";\n\nfunction parsePdfDate(pdfDate?: string | null): string | null {\n  if (!pdfDate) return null;\n  // PDF date format: D:YYYYMMDDHHmmSSOHH'mm'\n  // Example: D:20240102153045-08'00'\n  const m = pdfDate.match(\n    /^D:(\\d{4})(\\d{2})?(\\d{2})?(\\d{2})?(\\d{2})?(\\d{2})?([Zz]|([+\\-])(\\d{2})'?(\\d{2})'?)?$/,\n  );\n  if (!m) return null;\n\n  const [_, y, mo = \"01\", d = \"01\", h = \"00\", mi = \"00\", s = \"00\", z, sign, tzH, tzM] = m;\n  const yyyy = y;\n  const MM = mo.padStart(2, \"0\");\n  const dd = d.padStart(2, \"0\");\n  const HH = h.padStart(2, \"0\");\n  const MMm = mi.padStart(2, \"0\");\n  const SS = s.padStart(2, \"0\");\n\n  let offset = \"Z\";\n  if (z && z.toUpperCase() !== \"Z\" && tzH && tzM) {\n    offset = `${sign}${tzH}:${tzM}`;\n  }\n  // Build ISO string\n  const iso = `${yyyy}-${MM}-${dd}T${HH}:${MMm}:${SS}${offset}`;\n  const date = new Date(iso);\n  return isNaN(date.getTime()) ? null : date.toISOString();\n}\n\ntype HtmlLikeMetadata = {\n  title: string | null;\n  language: string | null;\n  urlSource: string | null;\n  timestamp: string;\n  description: string | null;\n  keywords: string | null;\n  author: string | null;\n\n  ogTitle: string | null;\n  ogDescription: string | null;\n  ogImage: string | null;\n  ogUrl: string | null;\n  ogSiteName: string | null;\n\n  articleAuthor: string | null;\n  publishedTime: string | null;\n  modifiedTime: string | null;\n\n  canonical: string | null;\n  favicon: string | null;\n\n  jsonLd: any[];\n  statusCode: number;\n};\n\nexport function extractLinksFromConvertedHtml(html: string): { url: string; text: string }[] {\n  const $ = loadHtml(html);\n  return $(\"a[href]\")\n    .map((_, a) => {\n      const url = $(a).attr(\"href\") || \"\";\n      const text = $(a).text()?.trim() || \"\";\n      return { url, text };\n    })\n    .get();\n}\n\nexport function buildHtmlLikeMetadataFromPdf(\n  pdfMeta: any,\n  opts: { urlSource?: string | null; statusCode?: number; htmlForFallback?: string | null },\n): HtmlLikeMetadata {\n  const { urlSource = null, statusCode = 200, htmlForFallback = null } = opts;\n\n  // Try to get a title from meta, fallback to <title> in converted HTML\n  let htmlTitle: string | null = null;\n  if (htmlForFallback) {\n    const $ = loadHtml(htmlForFallback);\n    const t = $(\"title\").first().text()?.trim();\n    htmlTitle = t || null;\n  }\n\n  const title = pdfMeta?.title || htmlTitle || null;\n  const author = pdfMeta?.author || null;\n  const description = pdfMeta?.subject || null;\n\n  // Keywords might be array or string depending on library\n  let keywords: string | null = null;\n  if (Array.isArray(pdfMeta?.keywords)) {\n    keywords = pdfMeta.keywords.join(\", \");\n  } else if (typeof pdfMeta?.keywords === \"string\") {\n    keywords = pdfMeta.keywords;\n  }\n\n  // XMP/DC language if exposed; often not present\n  const language = pdfMeta?.language || pdfMeta?.[\"dc:language\"] || null;\n\n  const publishedTime =\n    parsePdfDate(pdfMeta?.creationDate || pdfMeta?.CreationDate || pdfMeta?.[\"xmp:CreateDate\"]) ||\n    null;\n  const modifiedTime =\n    parsePdfDate(pdfMeta?.modDate || pdfMeta?.ModDate || pdfMeta?.[\"xmp:ModifyDate\"]) || null;\n\n  let origin: string | null = null;\n  let host: string | null = null;\n  if (urlSource) {\n    try {\n      const u = new URL(urlSource);\n      origin = u.origin;\n      host = u.hostname;\n    } catch {}\n  }\n\n  return {\n    title,\n    language,\n    urlSource,\n    timestamp: new Date().toISOString(),\n\n    description,\n    keywords,\n    author,\n\n    ogTitle: title,\n    ogDescription: description,\n    ogImage: null,\n    ogUrl: urlSource,\n    ogSiteName: host,\n\n    articleAuthor: author,\n    publishedTime,\n    modifiedTime,\n\n    canonical: urlSource,\n    favicon: origin ? `${origin}/favicon.ico` : null,\n\n    jsonLd: [],\n    statusCode,\n  };\n}\n"
  },
  {
    "path": "api/src/utils/scrape/plugins/highlightedCodeBlock.ts",
    "content": "// Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js\n\nimport TurndownService from \"@joplin/turndown\";\n\nconst highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;\n\nexport default function highlightedCodeBlock(turndownService: TurndownService) {\n  turndownService.addRule(\"highlightedCodeBlock\", {\n    filter: function (node) {\n      const firstChild = node.firstChild as HTMLElement;\n      return (\n        node.nodeName === \"DIV\" &&\n        highlightRegExp.test(node.className) &&\n        firstChild &&\n        firstChild.nodeName === \"PRE\"\n      );\n    },\n    replacement: function (content, node, options) {\n      const className = (node as HTMLElement).className || \"\";\n      const language = (className.match(highlightRegExp) || [null, \"\"])[1];\n\n      return (\n        \"\\n\\n\" +\n        options.fence +\n        language +\n        \"\\n\" +\n        (node.firstChild as HTMLElement).textContent +\n        \"\\n\" +\n        options.fence +\n        \"\\n\\n\"\n      );\n    },\n  });\n}\n"
  },
  {
    "path": "api/src/utils/scrape/plugins/inlineLink.ts",
    "content": "// Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js\n\nimport TurndownService from \"@joplin/turndown\";\n\nexport default function inlineLink(turndownService: TurndownService) {\n  turndownService.addRule(\"inlineLink\", {\n    filter: function (node, options) {\n      return (\n        options.linkStyle === \"inlined\" && node.nodeName === \"A\" && !!node.getAttribute(\"href\")\n      );\n    },\n    replacement: function (content, node) {\n      const href = (node as HTMLElement).getAttribute(\"href\")?.trim();\n      const title = (node as HTMLElement).title ? ' \"' + (node as HTMLElement).title + '\"' : \"\";\n      return \"[\" + content.trim() + \"](\" + href + title + \")\\n\";\n    },\n  });\n}\n"
  },
  {
    "path": "api/src/utils/scrape/plugins/strikethrough.ts",
    "content": "// Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js\n\nimport TurndownService from \"@joplin/turndown\";\n\nexport default function taskListItems(turndownService: TurndownService) {\n  turndownService.addRule(\"taskListItems\", {\n    filter: function (node) {\n      const parent = node.parentNode as HTMLElement;\n      const grandparent = parent.parentNode as HTMLElement;\n      return (\n        (node as HTMLInputElement).type === \"checkbox\" &&\n        (parent.nodeName === \"LI\" ||\n          // Handles the case where the label contains the checkbox. For example,\n          // <label><input ...> ...label text...</label>\n          (parent.nodeName === \"LABEL\" && grandparent && grandparent.nodeName === \"LI\"))\n      );\n    },\n    replacement: function (content, node) {\n      return ((node as HTMLInputElement).checked ? \"[x]\" : \"[ ]\") + \" \";\n    },\n  });\n}\n"
  },
  {
    "path": "api/src/utils/scrape/plugins/table.ts",
    "content": "// Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js\n\nimport TurndownService from \"@joplin/turndown\";\nimport { isCodeBlock } from \"./utilities.js\";\n\nvar indexOf = Array.prototype.indexOf;\nvar every = Array.prototype.every;\nvar rules: Record<string, any> = {};\nvar alignMap = { left: \":---\", right: \"---:\", center: \":---:\" };\n\n// We need to cache the result of tableShouldBeSkipped() as it is expensive.\n// Caching it means we went from about 9000 ms for rendering down to 90 ms.\n// Fixes https://github.com/laurent22/joplin/issues/6736\nconst tableShouldBeSkippedCache_ = new WeakMap();\n\nfunction getAlignment(node) {\n  return node ? (node.getAttribute(\"align\") || node.style.textAlign || \"\").toLowerCase() : \"\";\n}\n\nfunction getBorder(alignment) {\n  return alignment ? alignMap[alignment] : \"---\";\n}\n\nfunction getColumnAlignment(table, columnIndex) {\n  var votes = {\n    left: 0,\n    right: 0,\n    center: 0,\n    \"\": 0,\n  };\n\n  var align = \"\";\n\n  for (var i = 0; i < table.rows.length; ++i) {\n    var row = table.rows[i];\n    if (columnIndex < row.childNodes.length) {\n      var cellAlignment = getAlignment(row.childNodes[columnIndex]);\n      ++votes[cellAlignment];\n\n      if (votes[cellAlignment] > votes[align]) {\n        align = cellAlignment;\n      }\n    }\n  }\n\n  return align;\n}\n\nfunction extractTextFromCell(cellNode: HTMLElement): string {\n  const uiComponentTags = new Set([\"BUTTON\", \"SVG\", \"INPUT\", \"SELECT\", \"TEXTAREA\", \"FORM\"]);\n\n  function getTextContent(node: Node): string {\n    if (node.nodeType === node.TEXT_NODE) {\n      return node.textContent || \"\";\n    }\n\n    if (node.nodeType === node.ELEMENT_NODE) {\n      const element = node as HTMLElement;\n\n      if (uiComponentTags.has(element.tagName)) {\n        return \"\";\n      }\n\n      let text = \"\";\n      for (const child of element.childNodes) {\n        text += getTextContent(child);\n      }\n      return text;\n    }\n\n    return \"\";\n  }\n\n  return getTextContent(cellNode).trim().replace(/\\s+/g, \" \");\n}\n\nrules.tableCell = {\n  filter: [\"th\", \"td\"],\n  replacement: function (content, node) {\n    if (tableShouldBeSkipped(nodeParentTable(node))) return content;\n\n    // Extract only text content from complex UI components\n    const cleanContent = extractTextFromCell(node as HTMLElement);\n    return cell(cleanContent, node);\n  },\n};\n\nrules.tableRow = {\n  filter: \"tr\",\n  replacement: function (content, node) {\n    const parentTable = nodeParentTable(node);\n    if (tableShouldBeSkipped(parentTable)) return content;\n\n    var borderCells = \"\";\n\n    if (isHeadingRow(node)) {\n      const colCount = tableColCount(parentTable);\n      for (var i = 0; i < colCount; i++) {\n        const childNode = i < node.childNodes.length ? node.childNodes[i] : null;\n        var border = getBorder(getColumnAlignment(parentTable, i));\n        borderCells += cell(border, childNode, i);\n      }\n    }\n    return \"\\n\" + content + (borderCells ? \"\\n\" + borderCells : \"\");\n  },\n};\n\nrules.table = {\n  filter: function (node: Node, options: any) {\n    return node.nodeName === \"TABLE\";\n  },\n\n  replacement: function (content: string, node: Node) {\n    // Only convert tables that can result in valid Markdown\n    // Other tables are kept as HTML using `keep` (see below).\n    if (tableShouldBeHtml(node)) {\n      return `\\n\\n${(node as HTMLElement).outerHTML}\\n\\n`;\n    } else {\n      if (tableShouldBeSkipped(node)) return content;\n\n      // Ensure there are no blank lines\n      content = content.replace(/\\n+/g, \"\\n\");\n\n      // If table has no heading, add an empty one so as to get a valid Markdown table\n      var secondLine: string[] | string = content.trim().split(\"\\n\");\n      if (secondLine.length >= 2) secondLine = secondLine[1];\n      var secondLineIsDivider = /\\| :?---/.test(secondLine as string);\n\n      var columnCount = tableColCount(node);\n      var emptyHeader = \"\";\n      if (columnCount && !secondLineIsDivider) {\n        emptyHeader = \"|\" + \"    |\".repeat(columnCount) + \"\\n\" + \"|\";\n        for (var columnIndex = 0; columnIndex < columnCount; ++columnIndex) {\n          emptyHeader += \" \" + getBorder(getColumnAlignment(node, columnIndex)) + \" |\";\n        }\n      }\n\n      const captionContent = (node as HTMLTableElement).caption\n        ? (node as HTMLTableElement).caption?.textContent || \"\"\n        : \"\";\n      const caption = captionContent ? `${captionContent}\\n\\n` : \"\";\n      const tableContent = `${emptyHeader}${content}`.trimStart();\n      return `\\n\\n${caption}${tableContent}\\n\\n`;\n    }\n  },\n};\n\nrules.tableCaption = {\n  filter: [\"caption\"],\n  replacement: () => \"\",\n};\n\nrules.tableColgroup = {\n  filter: [\"colgroup\", \"col\"],\n  replacement: () => \"\",\n};\n\nrules.tableSection = {\n  filter: [\"thead\", \"tbody\", \"tfoot\"],\n  replacement: function (content) {\n    return content;\n  },\n};\n\n// A tr is a heading row if:\n// - the parent is a THEAD\n// - or if its the first child of the TABLE or the first TBODY (possibly\n//   following a blank THEAD)\n// - and every cell is a TH\nfunction isHeadingRow(tr) {\n  var parentNode = tr.parentNode;\n  return (\n    parentNode.nodeName === \"THEAD\" ||\n    (parentNode.firstChild === tr &&\n      (parentNode.nodeName === \"TABLE\" || isFirstTbody(parentNode)) &&\n      every.call(tr.childNodes, function (n) {\n        return n.nodeName === \"TH\";\n      }))\n  );\n}\n\nfunction isFirstTbody(element) {\n  var previousSibling = element.previousSibling;\n  return (\n    element.nodeName === \"TBODY\" &&\n    (!previousSibling ||\n      (previousSibling.nodeName === \"THEAD\" && /^\\s*$/i.test(previousSibling.textContent)))\n  );\n}\n\nfunction cell(content: string, node: Node, index: number | null = null) {\n  if (index === null) index = indexOf.call(node.parentNode?.childNodes, node);\n  var prefix = \" \";\n  if (index === 0) prefix = \"| \";\n  let filteredContent = content.trim().replace(/\\n\\r/g, \"<br>\").replace(/\\n/g, \"<br>\");\n  filteredContent = filteredContent.replace(/\\|+/g, \"\\\\|\");\n  while (filteredContent.length < 3) filteredContent += \" \";\n  if (node) filteredContent = handleColSpan(filteredContent, node, \" \");\n  return prefix + filteredContent + \" |\";\n}\n\nfunction nodeContainsTable(node) {\n  if (!node.childNodes) return false;\n\n  for (let i = 0; i < node.childNodes.length; i++) {\n    const child = node.childNodes[i];\n    if (child.nodeName === \"TABLE\") return true;\n    if (nodeContainsTable(child)) return true;\n  }\n  return false;\n}\n\nconst nodeContains = (node: Node, types: string | string[]) => {\n  if (!node.childNodes) return false;\n\n  for (let i = 0; i < node.childNodes.length; i++) {\n    const child = node.childNodes[i];\n    if (types === \"code\" && isCodeBlock(child as HTMLElement)) return true;\n    if (types.includes(child.nodeName)) return true;\n    if (nodeContains(child, types)) return true;\n  }\n\n  return false;\n};\n\nconst tableShouldBeHtml = (tableNode) => {\n  const possibleTags = [\"UL\", \"OL\", \"H1\", \"H2\", \"H3\", \"H4\", \"H5\", \"H6\", \"HR\", \"BLOCKQUOTE\"];\n\n  // In general we should leave as HTML tables that include other tables. The\n  // exception is with the Web Clipper when we import a web page with a layout\n  // that's made of HTML tables. In that case we have this logic of removing the\n  // outer table and keeping only the inner ones. For the Rich Text editor\n  // however we always want to keep nested tables.\n  possibleTags.push(\"TABLE\");\n\n  return nodeContains(tableNode, \"code\") || nodeContains(tableNode, possibleTags);\n};\n\n// Various conditions under which a table should be skipped - i.e. each cell\n// will be rendered one after the other as if they were paragraphs.\nfunction tableShouldBeSkipped(tableNode) {\n  const cached = tableShouldBeSkippedCache_.get(tableNode);\n  if (cached !== undefined) return cached;\n\n  const result = tableShouldBeSkipped_(tableNode);\n\n  tableShouldBeSkippedCache_.set(tableNode, result);\n  return result;\n}\n\nfunction tableShouldBeSkipped_(tableNode) {\n  if (!tableNode) return true;\n  if (!tableNode.rows) return true;\n  if (tableNode.rows.length === 1 && tableNode.rows[0].childNodes.length <= 1) return true; // Table with only one cell\n  if (nodeContainsTable(tableNode)) return true;\n  return false;\n}\n\nfunction nodeParentTable(node) {\n  let parent = node.parentNode;\n  while (parent.nodeName !== \"TABLE\") {\n    parent = parent.parentNode;\n    if (!parent) return null;\n  }\n  return parent;\n}\n\nfunction handleColSpan(content, node, emptyChar) {\n  const colspan = node.getAttribute(\"colspan\") || 1;\n  for (let i = 1; i < colspan; i++) {\n    content += \" | \" + emptyChar.repeat(3);\n  }\n  return content;\n}\n\nfunction tableColCount(node) {\n  let maxColCount = 0;\n  for (let i = 0; i < node.rows.length; i++) {\n    const row = node.rows[i];\n    const colCount = row.childNodes.length;\n    if (colCount > maxColCount) maxColCount = colCount;\n  }\n  return maxColCount;\n}\n\nexport default function tables(turndownService: TurndownService) {\n  turndownService.keep(function (node) {\n    if (node.nodeName === \"TABLE\" && tableShouldBeHtml(node)) return true;\n    return false;\n  });\n  for (var key in rules) turndownService.addRule(key, rules[key]);\n}\n"
  },
  {
    "path": "api/src/utils/scrape/plugins/taskListItems.ts",
    "content": "// Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js\n\nimport TurndownService from \"@joplin/turndown\";\n\nexport default function strikethrough(turndownService: TurndownService) {\n  turndownService.addRule(\"strikethrough\", {\n    filter: [\"del\", \"s\", \"strike\"] as unknown as (keyof HTMLElementTagNameMap)[],\n    replacement: function (content) {\n      return \"~~\" + content + \"~~\";\n    },\n  });\n}\n"
  },
  {
    "path": "api/src/utils/scrape/plugins/utilities.ts",
    "content": "// Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js\n\nimport css, { CssDeclarationAST, CssFontFaceAST } from \"@adobe/css-tools\";\n\nexport function isCodeBlockSpecialCase1(node: Node) {\n  const parent = node.parentNode;\n  if (!parent) return false;\n  return (\n    (parent as HTMLElement).classList &&\n    (parent as HTMLElement).classList.contains(\"code\") &&\n    (parent as HTMLElement).nodeName === \"TD\" &&\n    (node as HTMLElement).nodeName === \"PRE\"\n  );\n}\n\nexport function isCodeBlockSpecialCase2(node: Node) {\n  if (node.nodeName !== \"PRE\") return false;\n\n  const style = (node as HTMLElement).getAttribute(\"style\");\n  if (!style) return false;\n  const o = css.parse(\"pre {\" + style + \"}\");\n  if (!o.stylesheet.rules.length) return;\n  const fontFamily = (o.stylesheet.rules[0] as CssFontFaceAST).declarations.find(\n    (d) => (d as CssDeclarationAST).property.toLowerCase() === \"font-family\",\n  );\n  if (!fontFamily || !(fontFamily as CssDeclarationAST).value) return false;\n  const isMonospace =\n    (fontFamily as CssDeclarationAST).value\n      .split(\",\")\n      .map((e) => e.trim().toLowerCase())\n      .indexOf(\"monospace\") >= 0;\n  return isMonospace;\n}\n\nexport function isCodeBlock(node: Node) {\n  if (isCodeBlockSpecialCase1(node) || isCodeBlockSpecialCase2(node)) return true;\n\n  return (\n    (node as HTMLElement).nodeName === \"PRE\" &&\n    (node as HTMLElement).firstChild &&\n    (node as HTMLElement).firstChild?.nodeName === \"CODE\"\n  );\n}\n"
  },
  {
    "path": "api/src/utils/scrape/readability.ts",
    "content": "import { Defuddle } from \"defuddle/node\";\n\nexport const getDefuddleContent = async (htmlString: string) => {\n  const defuddle = await Defuddle(htmlString, undefined, {\n    debug: false,\n    markdown: false,\n  });\n\n  return defuddle;\n};\n"
  },
  {
    "path": "api/src/utils/scrape/safeGoTo.ts",
    "content": "import { Page, HTTPResponse } from \"puppeteer-core\";\n\n/**\n * Navigates to a URL and ignores net::ERR_ABORTED if the main-frame response is a PDF.\n * Returns { response, isPdf, pdfResponse }.\n *\n * - response: the normal Puppeteer Response from page.goto (null if it aborted on a PDF)\n * - isPdf: boolean indicating if the main-frame response was a PDF\n * - pdfResponse: the Response for the PDF (so you can buffer() it, if desired)\n */\nexport async function safeGoto(page: Page, url: string, options = {}) {\n  let pdfResponse: HTTPResponse | null = null;\n\n  const onResponse = (res: HTTPResponse) => {\n    // Only consider main-frame document navigations\n    const req = res.request();\n    const isMainFrameDoc = req.resourceType() === \"document\" && req.frame() === page.mainFrame();\n\n    if (!isMainFrameDoc) return;\n\n    const ct = (res.headers()[\"content-type\"] || \"\").toLowerCase();\n    console.log(\"content-type\", ct);\n    if (ct.includes(\"application/pdf\")) {\n      pdfResponse = res;\n    }\n  };\n\n  page.on(\"response\", onResponse);\n\n  try {\n    const resp = await page.goto(url, options);\n    return { response: resp, isPdf: !!pdfResponse, pdfResponse };\n  } catch (err: any) {\n    const message = String((err && err.message) || \"\");\n    // If we detected a PDF and Chromium aborted the navigation, swallow it\n    if (pdfResponse && message.includes(\"net::ERR_ABORTED\")) {\n      return { response: null, isPdf: true, pdfResponse };\n    }\n    throw err;\n  } finally {\n    page.off(\"response\", onResponse);\n  }\n}\n"
  },
  {
    "path": "api/src/utils/scrape/transformHtml.ts",
    "content": "import { JSDOM } from \"jsdom\";\n\nexport const transformHtml = (htmlContent: string, baseUrl?: string): string => {\n  const dom = new JSDOM(htmlContent);\n  const document = dom.window.document;\n\n  optimizeImages(document);\n\n  if (baseUrl) {\n    normalizeUrls(document, baseUrl);\n  }\n\n  return document.documentElement.outerHTML;\n};\n\nconst optimizeImages = (document: Document) => {\n  const imagesWithSrcset = document.querySelectorAll(\"img[srcset]\");\n\n  imagesWithSrcset.forEach((img) => {\n    const element = img as HTMLImageElement;\n    const srcsetValue = element.getAttribute(\"srcset\");\n    if (!srcsetValue) return;\n\n    const imageSources = srcsetValue.split(\",\").map((entry) => {\n      const parts = entry.trim().split(\" \");\n      return {\n        url: parts[0],\n        size: parseInt((parts[1] ?? \"1x\").slice(0, -1), 10),\n        isPixelDensity: (parts[1] ?? \"\").endsWith(\"x\"),\n      };\n    });\n\n    const currentSrc = element.getAttribute(\"src\");\n    if (imageSources.every((source) => source.isPixelDensity) && currentSrc) {\n      imageSources.push({\n        url: currentSrc,\n        size: 1,\n        isPixelDensity: true,\n      });\n    }\n\n    imageSources.sort((a, b) => b.size - a.size);\n\n    const bestSource = imageSources[0];\n    if (bestSource) {\n      element.setAttribute(\"src\", bestSource.url);\n      element.removeAttribute(\"srcset\");\n    }\n  });\n};\n\nconst normalizeUrls = (document: Document, baseUrl: string) => {\n  try {\n    const urlBase = new URL(baseUrl);\n\n    const processElements = (selector: string, attribute: string) => {\n      const elements = document.querySelectorAll(selector);\n      elements.forEach((element) => {\n        try {\n          const currentValue = element.getAttribute(attribute);\n          if (currentValue) {\n            element.setAttribute(attribute, new URL(currentValue, urlBase).href);\n          }\n        } catch {}\n      });\n    };\n\n    processElements(\"img[src]\", \"src\");\n    processElements(\"a[href]\", \"href\");\n    processElements(\"link[href]\", \"href\");\n    processElements(\"video[src]\", \"src\");\n    processElements(\"audio[src]\", \"src\");\n    processElements(\"source[src]\", \"src\");\n  } catch {}\n};\n"
  },
  {
    "path": "api/src/utils/size.ts",
    "content": "export const KB = 1000;\nexport const MB = 1000 * KB; // Most proxies use MB, not MiB\n"
  },
  {
    "path": "api/src/utils/text.ts",
    "content": "export function titleCase(input: string): string {\n  if (!input || typeof input !== \"string\") {\n    return \"\";\n  }\n  return input.charAt(0).toUpperCase() + input.slice(1);\n}\n"
  },
  {
    "path": "api/src/utils/url.ts",
    "content": "import { env } from \"../env.js\";\n\n/**\n * Returns the appropriate protocol based on the protocol type and HTTPS setting\n * @param protocolType 'http' or 'ws' - base protocol type\n * @returns The protocol string with or without 's' suffix based on env.USE_SSL\n */\nfunction getProtocol(protocolType: \"http\" | \"ws\"): string {\n  return env.USE_SSL ? `${protocolType}s` : protocolType;\n}\n\n/**\n * Returns the base URL for the server, handling DOMAIN vs HOST:PORT appropriately\n * @param protocolType 'http' or 'ws' - determines the protocol prefix\n * @returns Formatted base URL with appropriate protocol and trailing slash\n */\nexport function getBaseUrl(protocolType: \"http\" | \"ws\" = \"http\"): string {\n  const baseUrl = env.DOMAIN ?? `${env.HOST}:${env.PORT}`;\n  const protocol = getProtocol(protocolType);\n  return `${protocol}://${baseUrl}/`;\n}\n\n/**\n * Returns a fully qualified URL with the given path\n * @param path The path to append to the base URL\n * @param protocolType 'http' or 'ws' - determines the protocol prefix\n * @returns Formatted URL with appropriate protocol\n */\nexport function getUrl(path: string, protocolType: \"http\" | \"ws\" = \"http\"): string {\n  const base = getBaseUrl(protocolType);\n  // Handle paths that might already have a leading slash\n  const formattedPath = path.startsWith(\"/\") ? path.substring(1) : path;\n  return `${base}${formattedPath}`;\n}\n\n/**\n * Normalizes a URL by adding https:// protocol if missing\n * @param url The URL to normalize\n * @returns The normalized URL with proper protocol, or null if invalid\n */\nexport function normalizeUrl(url: string): string | null {\n  if (!url || typeof url !== \"string\") {\n    return null;\n  }\n\n  const trimmedUrl = url.trim();\n  if (!trimmedUrl) {\n    return null;\n  }\n\n  if (trimmedUrl.startsWith(\"http://\") || trimmedUrl.startsWith(\"https://\")) {\n    return trimmedUrl;\n  }\n\n  const normalizedUrl = `https://${trimmedUrl}`;\n\n  try {\n    new URL(normalizedUrl);\n    return normalizedUrl;\n  } catch {\n    return null;\n  }\n}\n"
  },
  {
    "path": "api/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"module\": \"Node16\",\n    \"target\": \"ESNext\",\n    \"moduleResolution\": \"Node16\",\n    \"skipLibCheck\": true,\n    \"sourceMap\": true,\n    \"outDir\": \"build\",\n    \"forceConsistentCasingInFileNames\": true,\n    \"noImplicitAny\": false,\n    \"strict\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true\n  },\n  \"include\": [\"src\"],\n  \"files\": [\n    \"./src/types/fastify.d.ts\",\n    \"src/services/cdp/plugins/pptr-extensions.d.ts\",\n    \"src/types/turndown.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "api/tsconfig.test.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"noEmit\": true\n  },\n  \"include\": [\"**/*.ts\"]\n}\n"
  },
  {
    "path": "commitlint.config.cjs",
    "content": "module.exports = {\n  extends: ['@commitlint/config-conventional'],\n};\n"
  },
  {
    "path": "docker-compose.dev.yml",
    "content": "services:\n  api:\n    build:\n      context: .\n      dockerfile: ./api/Dockerfile\n      args:\n        NODE_VERSION: 22.13.0\n    ports:\n      - \"3000:3000\"\n      - \"9223:9223\"\n    environment:\n      - DOMAIN=${DOMAIN:-localhost:3000}\n      - CDP_DOMAIN=${CDP_DOMAIN:-localhost:9223}\n    volumes:\n      - ./.cache:/app/.cache\n    networks:\n      - steel-network\n\n  ui:\n    build:\n      context: .\n      dockerfile: ./ui/Dockerfile\n    ports:\n      - \"5173:80\"\n    environment:\n      - API_URL=${API_URL:-http://api:3000}\n    depends_on:\n      - api\n    networks:\n      - steel-network\n\nnetworks:\n  steel-network:\n    name: steel-network\n    driver: bridge"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  api:\n    image: ghcr.io/steel-dev/steel-browser-api:latest\n    ports:\n      - \"3000:3000\"\n      - \"9223:9223\"\n    volumes:\n      - logs:/data/logs\n      - exports:/tmp/steel-browser-exports\n      - ./.cache:/app/.cache\n    environment:\n      - DOMAIN=${DOMAIN:-localhost:3000}\n      - CDP_DOMAIN=${CDP_DOMAIN:-localhost:9223}\n      - LOG_STORAGE_ENABLED=true\n      - LOG_STORAGE_PATH=/data/logs/browser-logs.duckdb\n    networks:\n      - steel-network\n\n  ui:\n    image: ghcr.io/steel-dev/steel-browser-ui:latest\n    ports:\n      - \"5173:80\"\n    environment:\n      - API_URL=${API_URL:-http://api:3000}\n    depends_on:\n      - api\n    networks:\n      - steel-network\n\nnetworks:\n  steel-network:\n    name: steel-network\n    driver: bridge\n\nvolumes:\n  logs:\n    driver: local\n  exports:\n    driver: local\n"
  },
  {
    "path": "docs/ARCHITECTURE.md",
    "content": "# Steel Browser Architecture\n\nThis document provides a comprehensive overview of Steel Browser's architecture, design decisions, and how the various components work together.\n\n## 🏗️ High-Level Architecture\n\nSteel Browser follows a modular, plugin-based architecture designed for extensibility and maintainability:\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│                        Steel Browser                        │\n├─────────────────────────────────────────────────────────────┤\n│  Frontend (React UI)           │  Backend (Fastify API)     │\n│  ├── Session Management        │  ├── CDP Service           │\n│  ├── Real-time Viewing         │  ├── Session Management    │\n│  ├── DevTools Integration      │  ├── File Storage          │\n│  └── Configuration UI          │  └── Plugin System         │\n├─────────────────────────────────────────────────────────────┤\n│                    Chrome/Chromium Browser                  │\n│  ├── Chrome DevTools Protocol (CDP)                         │\n│  ├── Browser Extensions                                     │\n│  └── Page Contexts                                          │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## 🔧 Core Components\n\n### 1. CDP Service (`api/src/services/cdp/cdp.service.ts`)\n\nThe Chrome DevTools Protocol (CDP) Service is the heart of Steel Browser, managing all browser interactions:\n\n**Responsibilities:**\n- Browser lifecycle management (launch, close, restart)\n- Page creation and navigation\n- WebSocket proxy for CDP connections\n- Plugin system coordination\n- Session state management\n- Context isolation and fingerprinting\n\n**Key Features:**\n```typescript\nclass CDPService extends EventEmitter {\n  // Browser management\n  async launch(options?: BrowserLauncherOptions): Promise<Browser>\n  async shutdown(): Promise<void>\n  async refreshPrimaryPage(): Promise<void>\n  \n  // Plugin system\n  registerPlugin(plugin: BasePlugin): void\n  unregisterPlugin(pluginName: string): boolean\n  \n  // Page management\n  async createPage(): Promise<Page>\n  async getPages(): Promise<Page[]>\n}\n```\n\n### 2. Plugin System\n\nSteel Browser's plugin architecture allows for extensible functionality without modifying core code.\n\n#### Base Plugin (`api/src/services/cdp/plugins/core/base-plugin.ts`)\n\n```typescript\nabstract class BasePlugin {\n  // Lifecycle hooks\n  async onBrowserLaunch(browser: Browser): Promise<void>\n  async onPageCreated(page: Page): Promise<void>\n  async onPageNavigate(page: Page): Promise<void>\n  async onPageUnload(page: Page): Promise<void>\n  async onBrowserClose(browser: Browser): Promise<void>\n  async onBeforePageClose(page: Page): Promise<void>\n  async onShutdown(): Promise<void>\n}\n```\n\n#### Plugin Manager (`api/src/services/cdp/plugins/core/plugin-manager.ts`)\n\nCoordinates plugin lifecycle and ensures error isolation:\n\n- **Registration**: Manages plugin registration and dependency injection\n- **Event Distribution**: Notifies all plugins of browser events\n- **Error Handling**: Isolates plugin errors to prevent system crashes\n- **Lifecycle Management**: Coordinates plugin startup and shutdown\n\n### 3. Session Management (`api/src/services/session.service.ts`)\n\nManages browser sessions with isolated contexts:\n\n**Features:**\n- Session creation with custom configurations\n- Context isolation (cookies, localStorage, sessionStorage)\n- Resource cleanup and garbage collection\n- Session persistence and restoration\n- Concurrent session management\n\n```typescript\ninterface SessionConfig {\n  proxy?: ProxyConfig;\n  userAgent?: string;\n  viewport?: { width: number; height: number };\n  extensions?: string[];\n  fingerprint?: FingerprintOptions;\n}\n```\n\n### 4. File Storage Service (`api/src/services/file.service.ts`)\n\nHandles file operations with session-scoped storage:\n\n- **Upload Management**: Handles multipart file uploads\n- **Download Coordination**: Manages browser downloads\n- **Storage Isolation**: Session-scoped file storage\n- **Cleanup**: Automatic file cleanup on session end\n\n## 🔌 Plugin Architecture Deep Dive\n\n### Plugin Lifecycle\n\n1. **Registration**: Plugins register with the PluginManager\n2. **Initialization**: Service dependency injection\n3. **Event Handling**: Respond to browser lifecycle events\n4. **Cleanup**: Graceful shutdown and resource cleanup\n\n### Event Flow\n\n```\nBrowser Launch → Plugin.onBrowserLaunch()\n     ↓\nPage Created → Plugin.onPageCreated()\n     ↓\nPage Navigate → Plugin.onPageNavigate()\n     ↓\nPage Unload → Plugin.onPageUnload()\n     ↓\nPage Close → Plugin.onBeforePageClose()\n     ↓\nBrowser Close → Plugin.onBrowserClose()\n     ↓\nSystem Shutdown → Plugin.onShutdown()\n```\n\n### Example Plugin Implementation\n\n```typescript\nimport { BasePlugin, PluginOptions } from '@steel-browser/api/cdp-plugin';\nimport { Browser, Page } from 'puppeteer-core';\n\nexport class AdBlockPlugin extends BasePlugin {\n  private blockedDomains: Set<string>;\n\n  constructor(options: PluginOptions & { blockedDomains?: string[] }) {\n    super({ name: 'ad-blocker', ...options });\n    this.blockedDomains = new Set(options.blockedDomains || []);\n  }\n\n  async onPageCreated(page: Page): Promise<void> {\n    await page.setRequestInterception(true);\n    \n    page.on('request', (request) => {\n      const url = new URL(request.url());\n      if (this.blockedDomains.has(url.hostname)) {\n        request.abort();\n      } else {\n        request.continue();\n      }\n    });\n  }\n}\n```\n\n## 🌐 API Architecture\n\n### Fastify Plugin System\n\nSteel Browser uses Fastify's plugin architecture for modular API design:\n\n```typescript\n// Main plugin registration\nawait fastify.register(steelBrowserPlugin, {\n  fileStorage: { maxSizePerSession: 100 * MB }\n});\n\n// Individual plugins\nawait fastify.register(browserInstancePlugin);\nawait fastify.register(sessionPlugin);\nawait fastify.register(fileStoragePlugin);\n```\n\n### Route Organization\n\nRoutes are organized by functionality:\n\n- **Actions** (`/v1/*`): Browser automation actions (scrape, screenshot, PDF)\n- **Sessions** (`/v1/sessions/*`): Session management\n- **CDP** (`/v1/cdp/*`): Direct CDP access\n- **Files** (`/v1/files/*`): File upload/download\n- **Selenium** (`/selenium/*`): Selenium WebDriver compatibility\n\n### Schema Validation\n\nAll API endpoints use Zod schemas for validation:\n\n```typescript\nconst ScrapeRequestSchema = z.object({\n  url: z.string().url().optional(),\n  delay: z.number().optional(),\n  format: z.array(z.enum(['html','cleaned_html','markdown','readability'])).optional(),\n  screenshot: z.boolean().optional(),\n  pdf: z.boolean().optional(),\n});\n```\n\n## 🎨 Frontend Architecture\n\n### React Component Structure\n\n```\nsrc/\n├── components/           # Reusable UI components\n│   ├── ui/              # Base UI components (buttons, inputs)\n│   ├── badges/          # Status badges\n│   ├── icons/           # Icon components\n│   └── sessions/        # Session-specific components\n├── containers/          # Page-level containers\n├── contexts/           # React contexts for state management\n├── hooks/              # Custom React hooks\n└── steel-client/       # Auto-generated API client\n```\n\n### State Management\n\n- **React Query**: Server state management and caching\n- **React Context**: Global application state\n- **Local State**: Component-specific state with hooks\n\n### Real-time Updates\n\nWebSocket connections provide real-time updates:\n\n```typescript\n// Session monitoring\nconst { data: sessions } = useQuery({\n  queryKey: ['sessions'],\n  queryFn: () => steelClient.sessions.getSessions(),\n  refetchInterval: 1000 // Real-time updates\n});\n```\n\n## 🔒 Security Architecture\n\n### Input Validation\n\n- **API Level**: Zod schema validation for all inputs\n- **Browser Level**: Content Security Policy (CSP) headers\n- **File Level**: File type validation and size limits\n\n### Context Isolation\n\nEach session runs in an isolated browser context:\n\n```typescript\nconst context = await browser.createIncognitoBrowserContext();\ncontext.setDefaultNavigationTimeout(30000);\ncontext.setDefaultTimeout(30000);\n```\n\n### Resource Limits\n\n- **Memory**: Browser process memory limits\n- **CPU**: Process CPU throttling\n- **Storage**: Session-scoped file storage limits\n- **Network**: Request rate limiting and proxy support\n\n## 📊 Performance Considerations\n\n### Browser Resource Management\n\n- **Process Isolation**: Each session in separate browser context\n- **Memory Cleanup**: Automatic page and context cleanup\n- **Connection Pooling**: Reuse CDP connections where possible\n\n### Caching Strategy\n\n- **Static Assets**: Long-term caching for UI assets\n- **API Responses**: Short-term caching for session data\n- **Browser Cache**: Configurable per-session browser caching\n\n### Scaling Considerations\n\nCurrent architecture supports:\n- **Vertical Scaling**: Multi-core CPU utilization\n- **Session Concurrency**: Multiple simultaneous sessions\n- **Resource Monitoring**: Memory and CPU usage tracking\n\nFuture scaling options:\n- **Horizontal Scaling**: Multiple Steel instances\n- **Load Balancing**: Session distribution\n- **Distributed Storage**: Shared file storage\n\n## 🧪 Testing Architecture\n\n### Test Structure (Planned)\n\n```\ntests/\n├── unit/               # Unit tests for individual components\n├── integration/        # API endpoint integration tests\n├── e2e/               # End-to-end browser automation tests\n└── performance/       # Load and performance tests\n```\n\n### Testing Strategy\n\n- **Unit Tests**: Core services and utilities\n- **Integration Tests**: API endpoints and database interactions\n- **E2E Tests**: Full browser automation workflows\n- **Performance Tests**: Load testing and benchmarking\n\n## 🔧 Configuration Management\n\n### Environment Variables\n\nConfiguration through environment variables:\n\n```typescript\nconst envSchema = z.object({\n  NODE_ENV: z.enum(['development', 'production', 'test']),\n  HOST: z.string().default('0.0.0.0'),\n  PORT: z.string().default('3000'),\n  CHROME_EXECUTABLE_PATH: z.string().optional(),\n  CHROME_HEADLESS: z.boolean().default(true),\n  // ... more configuration options\n});\n```\n\n### Runtime Configuration\n\n- **Browser Options**: Per-session browser configuration\n- **Plugin Configuration**: Dynamic plugin options\n- **Feature Flags**: Runtime feature toggling\n\n## 🚀 Deployment Architecture\n\n### Containerization\n\nMulti-stage Docker builds for optimization:\n\n```dockerfile\n# Build stage\nFROM node:22-slim AS build\n# ... build steps\n\n# Production stage  \nFROM node:22-slim AS production\n# ... production setup\n```\n\n### Service Dependencies\n\n- **Chrome/Chromium**: Browser engine\n- **Node.js**: Runtime environment\n- **Nginx**: Reverse proxy (in containers)\n- **File System**: Session storage\n\n## 🔄 Development Workflow\n\n### Hot Reloading\n\nDevelopment environment supports hot reloading:\n\n```bash\nnpm run dev  # Starts both API and UI with hot reload\n```\n\n### Debug Configuration\n\nBuilt-in debugging support:\n\n```bash\n# API debugging\nnode --inspect ./api/build/index.js\n\n# Enable verbose logging\nENABLE_VERBOSE_LOGGING=true npm run dev -w api\n```\n\n## 📈 Monitoring and Observability\n\n### Logging\n\nStructured logging with Pino:\n\n```typescript\nfastify.log.info({ \n  sessionId, \n  action: 'page_created',\n  url: page.url() \n}, 'New page created');\n```\n\n### Metrics (Planned)\n\n- **Session Metrics**: Creation, duration, success rates\n- **Performance Metrics**: Response times, resource usage\n- **Error Tracking**: Error rates and categorization\n\n### Health Checks\n\nBuilt-in health check endpoints:\n\n```typescript\n// Basic health check\nGET /health\n\n// Detailed readiness check\nGET /ready\n```\n\n---\n\nThis architecture provides a solid foundation for browser automation while maintaining flexibility for future enhancements and scaling requirements. "
  },
  {
    "path": "docs/DEVELOPMENT_SETUP.md",
    "content": "# Development Setup Guide\n\nThis guide provides comprehensive instructions for setting up a Steel Browser development environment.\n\n## 🎯 Prerequisites\n\n### System Requirements\n\n- **Operating System**: Linux, macOS, or Windows (with WSL2 recommended)\n- **RAM**: Minimum 8GB, recommended 16GB+\n- **Storage**: At least 10GB free space\n- **Network**: Stable internet connection for dependencies\n\n### Required Software\n\n#### 1. Node.js (Version 22+)\n\n**Linux/macOS:**\n```bash\n# Using Node Version Manager (recommended)\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash\nsource ~/.bashrc\nnvm install 22\nnvm use 22\n\n# Or using package manager\n# Ubuntu/Debian\nsudo apt update\nsudo apt install nodejs npm\n\n# macOS with Homebrew\nbrew install node@22\n```\n\n**Windows:**\n```powershell\n# Using Chocolatey\nchoco install nodejs --version=22.0.0\n\n# Or download from https://nodejs.org/\n```\n\n#### 2. Git\n\n**Linux:**\n```bash\nsudo apt install git  # Ubuntu/Debian\nsudo yum install git   # CentOS/RHEL\n```\n\n**macOS:**\n```bash\nbrew install git\n# or use Xcode Command Line Tools\nxcode-select --install\n```\n\n**Windows:**\n```powershell\nchoco install git\n# or download from https://git-scm.com/\n```\n\n#### 3. Chrome/Chromium Browser\n\n**Linux:**\n```bash\n# Ubuntu/Debian\nwget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -\necho \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" | sudo tee /etc/apt/sources.list.d/google-chrome.list\nsudo apt update\nsudo apt install google-chrome-stable\n\n# Or Chromium\nsudo apt install chromium-browser\n```\n\n**macOS:**\n```bash\nbrew install --cask google-chrome\n# or download from https://www.google.com/chrome/\n```\n\n**Windows:**\n```powershell\nchoco install googlechrome\n# or download from https://www.google.com/chrome/\n```\n\n#### 4. Docker (Optional but Recommended)\n\n**Linux:**\n```bash\n# Ubuntu/Debian\nsudo apt update\nsudo apt install docker.io docker-compose\nsudo usermod -aG docker $USER\nnewgrp docker\n\n# CentOS/RHEL\nsudo yum install docker docker-compose\nsudo systemctl start docker\nsudo systemctl enable docker\nsudo usermod -aG docker $USER\n```\n\n**macOS:**\n```bash\nbrew install --cask docker\n# or download Docker Desktop from https://www.docker.com/products/docker-desktop\n```\n\n**Windows:**\n```powershell\nchoco install docker-desktop\n# or download Docker Desktop from https://www.docker.com/products/docker-desktop\n```\n\n## 🚀 Quick Setup\n\n### 1. Clone the Repository\n\n```bash\n# Fork the repository first on GitHub, then clone your fork\ngit clone https://github.com/YOUR_USERNAME/steel-browser.git\ncd steel-browser\n\n# Add upstream remote\ngit remote add upstream https://github.com/steel-dev/steel-browser.git\n```\n\n### 2. Install Dependencies\n\n```bash\n# Install all workspace dependencies\nnpm install\n\n# Verify installation\nnpm list --depth=0\n```\n\n### 3. Build the Project\n\n```bash\n# Build all workspaces\nnpm run build\n\n# Or build individually\nnpm run build -w api\nnpm run build -w ui\n```\n\n### 4. Start Development Environment\n\n```bash\n# Start both API and UI in development mode\nnpm run dev\n\n# This will start:\n# - API server on http://localhost:3000\n# - UI server on http://localhost:5173\n```\n\n### 5. Verify Setup\n\n```bash\n# Test API\ncurl http://localhost:3000/v1/health\n\n# Test UI\nopen http://localhost:5173\n\n# Test REPL\ncd repl\nnpm start\n```\n\n## 🐳 Docker Development Setup\n\n### 1. Using Docker Compose (Recommended)\n\n```bash\n# Start development environment\ndocker-compose -f docker-compose.dev.yml up --build\n\n# Or in detached mode\ndocker-compose -f docker-compose.dev.yml up -d --build\n\n# View logs\ndocker-compose -f docker-compose.dev.yml logs -f\n```\n\n### 2. Individual Container Setup\n\n```bash\n# Build API container\ndocker build -t steel-browser-api -f ./api/Dockerfile .\n\n# Build UI container\ndocker build -t steel-browser-ui -f ./ui/Dockerfile .\n\n# Run API container\ndocker run -p 3000:3000 -p 9223:9223 steel-browser-api\n\n# Run UI container\ndocker run -p 5173:80 steel-browser-ui\n```\n\n## 🔧 Development Configuration\n\n### Environment Variables\n\nCreate a `.env` file in the root directory:\n\n```bash\n# .env\nNODE_ENV=development\nHOST=0.0.0.0\nPORT=3000\nCDP_REDIRECT_PORT=9223\n\n# Chrome Configuration\nCHROME_HEADLESS=false  # Set to true for headless mode\nCHROME_EXECUTABLE_PATH=/usr/bin/google-chrome  # Adjust path as needed\nENABLE_CDP_LOGGING=true\nENABLE_VERBOSE_LOGGING=true\n\n# Development Features\nDEBUG_CHROME_PROCESS=false\nLOG_CUSTOM_EMIT_EVENTS=true\n\n# UI Configuration\nAPI_URL=http://localhost:3000\n```\n\n### Chrome Configuration\n\n#### Finding Chrome Executable\n\n**Linux:**\n```bash\nwhich google-chrome\nwhich chromium-browser\n# Common paths:\n# /usr/bin/google-chrome\n# /usr/bin/chromium-browser\n```\n\n**macOS:**\n```bash\n# Common path:\n# /Applications/Google Chrome.app/Contents/MacOS/Google Chrome\n```\n\n**Windows:**\n```powershell\n# Common paths:\n# C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\n# C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe\n```\n\n#### Custom Chrome Arguments\n\n```bash\n# Add custom Chrome arguments\nexport CHROME_ARGS=\"--disable-web-security --disable-features=VizDisplayCompositor\"\n\n# Filter out problematic arguments\nexport FILTER_CHROME_ARGS=\"--disable-dev-shm-usage\"\n```\n\n## 🛠️ IDE Setup\n\n### Visual Studio Code\n\n#### Recommended Extensions\n\n```bash\n# Install VS Code extensions\ncode --install-extension ms-typescript.typescript\ncode --install-extension esbenp.prettier-vscode\ncode --install-extension bradlc.vscode-tailwindcss\ncode --install-extension ms-vscode.vscode-typescript-next\ncode --install-extension ms-vscode.vscode-eslint\n```\n\n#### Settings Configuration\n\nCreate `.vscode/settings.json`:\n\n```json\n{\n  \"typescript.preferences.importModuleSpecifier\": \"relative\",\n  \"editor.formatOnSave\": true,\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": true\n  },\n  \"typescript.preferences.includePackageJsonAutoImports\": \"on\",\n  \"files.exclude\": {\n    \"**/node_modules\": true,\n    \"**/build\": true,\n    \"**/dist\": true,\n    \"**/.cache\": true\n  }\n}\n```\n\n#### Debug Configuration\n\nCreate `.vscode/launch.json`:\n\n```json\n{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Debug API\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"program\": \"${workspaceFolder}/api/build/index.js\",\n      \"outFiles\": [\"${workspaceFolder}/api/build/**/*.js\"],\n      \"env\": {\n        \"NODE_ENV\": \"development\",\n        \"ENABLE_VERBOSE_LOGGING\": \"true\",\n        \"CHROME_HEADLESS\": \"false\"\n      },\n      \"console\": \"integratedTerminal\",\n      \"restart\": true,\n      \"runtimeArgs\": [\"--inspect\"]\n    },\n    {\n      \"name\": \"Debug Tests\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/vitest\",\n      \"args\": [\"run\"],\n      \"cwd\": \"${workspaceFolder}\",\n      \"console\": \"integratedTerminal\"\n    }\n  ]\n}\n```\n\n### WebStorm/IntelliJ IDEA\n\n#### Run Configurations\n\n1. **API Development**\n   - Type: Node.js\n   - JavaScript file: `api/build/index.js`\n   - Environment variables: `NODE_ENV=development;ENABLE_VERBOSE_LOGGING=true`\n\n2. **UI Development**\n   - Type: npm\n   - Command: `run`\n   - Scripts: `dev`\n   - Package.json: `ui/package.json`\n\n## 🧪 Testing Setup\n\n### Unit Testing\n\n```bash\n# Install testing dependencies (if not already installed)\nnpm install -D vitest @vitest/ui jsdom\n\n# Run tests\nnpm test\n\n# Run tests in watch mode\nnpm test -- --watch\n\n# Run tests with UI\nnpm test -- --ui\n```\n\n### Integration Testing\n\n```bash\n# Start test environment\nNODE_ENV=test npm run dev -w api\n\n# Run integration tests\nnpm run test:integration\n```\n\n### End-to-End Testing\n\n```bash\n# Install E2E testing framework\nnpm install -D playwright @playwright/test\n\n# Run E2E tests\nnpx playwright test\n\n# Run E2E tests with UI\nnpx playwright test --ui\n```\n\n## 📊 Development Tools\n\n### Code Quality\n\n```bash\n# Format code\nnpm run pretty -w api\nnpm run lint -w ui\n\n# Check types\nnpm run type-check -w api\nnpm run type-check -w ui\n\n# Pre-commit hooks (automatically set up with Husky)\nnpm run prepare\n```\n\n### Performance Monitoring\n\n```bash\n# Start with performance profiling\nnode --prof ./api/build/index.js\n\n# Generate performance report\nnode --prof-process isolate-*.log > performance.txt\n\n# Memory usage monitoring\nnode --inspect --expose-gc ./api/build/index.js\n```\n\n### Database/Storage Tools\n\n```bash\n# View session storage\nls -la ./db/data/\n\n# View file storage\nls -la ./files/\n\n# Clear storage\nrm -rf ./db/data/*\nrm -rf ./files/*\n```\n\n## 🔄 Development Workflow\n\n### Daily Development\n\n```bash\n# 1. Update your fork\ngit fetch upstream\ngit checkout main\ngit merge upstream/main\n\n# 2. Create feature branch\ngit checkout -b feature/my-new-feature\n\n# 3. Start development environment\nnpm run dev\n\n# 4. Make changes and test\n# ... develop your feature ...\n\n# 5. Run quality checks\nnpm run pretty -w api\nnpm run lint -w ui\nnpm run build\n\n# 6. Commit changes\ngit add .\ngit commit -m \"feat: add new feature\"\n\n# 7. Push and create PR\ngit push origin feature/my-new-feature\n```\n\n### Hot Reloading\n\nThe development environment supports hot reloading:\n\n- **API**: Uses `tsx watch` for automatic TypeScript compilation and restart\n- **UI**: Uses Vite's hot module replacement (HMR)\n- **Extensions**: Require manual rebuild (`npm run prepare:recorder -w api`)\n\n### Debugging Browser Issues\n\n```bash\n# Run Chrome in non-headless mode\nexport CHROME_HEADLESS=false\nnpm run dev -w api\n\n# Enable Chrome debugging\nexport DEBUG_CHROME_PROCESS=true\n\n# Connect to Chrome DevTools\n# Open http://localhost:9223 in your browser\n```\n\n## 🚨 Troubleshooting Development Setup\n\n### Common Issues\n\n#### 1. Node.js Version Mismatch\n\n```bash\n# Check current version\nnode --version\n\n# Switch to correct version\nnvm use 22\n\n# Set as default\nnvm alias default 22\n```\n\n#### 2. Permission Issues (Linux/macOS)\n\n```bash\n# Fix npm permissions\nsudo chown -R $(whoami) ~/.npm\nsudo chown -R $(whoami) /usr/local/lib/node_modules\n\n# Fix project permissions\nsudo chown -R $(whoami) ./node_modules\n```\n\n#### 3. Chrome Not Found\n\n```bash\n# Find Chrome installation\nwhich google-chrome\nwhich chromium-browser\n\n# Set Chrome path\nexport CHROME_EXECUTABLE_PATH=/path/to/chrome\n```\n\n#### 4. Port Conflicts\n\n```bash\n# Check what's using the port\nlsof -i :3000\nlsof -i :5173\n\n# Kill conflicting processes\nkill -9 $(lsof -t -i:3000)\n\n# Use different ports\nexport PORT=3001\n```\n\n#### 5. Memory Issues\n\n```bash\n# Increase Node.js memory limit\nexport NODE_OPTIONS=\"--max-old-space-size=4096\"\n\n# Monitor memory usage\nhtop\n```\n\n### Getting Help\n\nIf you encounter issues:\n\n1. Check the [Troubleshooting Guide](./TROUBLESHOOTING.md)\n2. Search existing GitHub issues\n3. Ask in our Discord community\n4. Create a detailed bug report\n\n## 📚 Next Steps\n\nAfter setting up your development environment:\n\n1. **Read the [Architecture Guide](./ARCHITECTURE.md)** to understand the system design\n2. **Explore the [Plugin Development Guide](./PLUGIN_DEVELOPMENT.md)** to create extensions\n3. **Check out the [Contributing Guide](../CONTRIBUTING.md)** for contribution guidelines\n4. **Browse the [Steel Cookbook](https://github.com/steel-dev/steel-cookbook)** for usage examples\n\n## 🎉 You're Ready!\n\nYour Steel Browser development environment is now set up and ready for development. Happy coding! 🚀\n\n---\n\n**Need help?** Join our [Discord community](https://discord.gg/steel-dev) for real-time support! "
  },
  {
    "path": "docs/PLUGIN_DEVELOPMENT.md",
    "content": "# Plugin Development Guide\n\nThis guide walks you through developing custom plugins for Steel Browser's extensible architecture.\n\n## 🚀 Quick Start\n\n### Creating Your First Plugin\n\n```typescript\nimport { BasePlugin, PluginOptions } from '@steel-browser/api/cdp-plugin';\nimport { Browser, Page } from 'puppeteer-core';\n\nexport class HelloWorldPlugin extends BasePlugin {\n  constructor(options: PluginOptions) {\n    super({ name: 'hello-world', ...options });\n  }\n\n  async onBrowserLaunch(browser: Browser): Promise<void> {\n    console.log('Hello from the browser launch event!');\n  }\n\n  async onPageCreated(page: Page): Promise<void> {\n    console.log(`New page created: ${page.url()}`);\n  }\n}\n\n// Usage\nconst plugin = new HelloWorldPlugin({});\ncdpService.registerPlugin(plugin);\n```\n\n## 🏗️ Plugin Architecture\n\n### Base Plugin Class\n\nAll plugins extend the `BasePlugin` abstract class:\n\n```typescript\nabstract class BasePlugin {\n  public name: string;\n  protected options: PluginOptions;\n  protected cdpService: CDPService | null;\n\n  constructor(options: PluginOptions);\n  public setService(service: CDPService): void;\n\n  // Lifecycle hooks (all optional)\n  public async onBrowserLaunch(browser: Browser): Promise<void> {}\n  public async onPageCreated(page: Page): Promise<void> {}\n  public async onPageNavigate(page: Page): Promise<void> {}\n  public async onPageUnload(page: Page): Promise<void> {}\n  public async onBrowserClose(browser: Browser): Promise<void> {}\n  public async onBeforePageClose(page: Page): Promise<void> {}\n  public async onShutdown(): Promise<void> {}\n}\n```\n\n### Plugin Options\n\n```typescript\ninterface PluginOptions {\n  name: string;\n  [key: string]: any; // Additional plugin-specific options\n}\n```\n\n## 🔄 Lifecycle Events\n\n### Event Order\n\n```\n1. onBrowserLaunch    - Browser process starts\n2. onPageCreated      - New page/tab created\n3. onPageNavigate     - Page navigates to URL\n4. onPageUnload       - Page unloads/navigates away\n5. onBeforePageClose  - Before page closes\n6. onBrowserClose     - Browser process closes\n7. onShutdown         - Plugin cleanup\n```\n\n### Event Details\n\n#### onBrowserLaunch(browser: Browser)\n- Called when the browser process starts\n- Use for browser-level configuration\n- Access to the Browser instance\n\n#### onPageCreated(page: Page)\n- Called when a new page/tab is created\n- Perfect for page-level setup (request interception, etc.)\n- Access to the Page instance\n\n#### onPageNavigate(page: Page)\n- Called before page navigation\n- Use for URL-based logic or navigation tracking\n\n#### onPageUnload(page: Page)\n- Called when page unloads or navigates away\n- Cleanup page-specific resources\n\n#### onBeforePageClose(page: Page)\n- Called before a page closes\n- Last chance for page cleanup\n\n#### onBrowserClose(browser: Browser)\n- Called when browser process closes\n- Browser-level cleanup\n\n#### onShutdown()\n- Called during plugin shutdown\n- Final cleanup opportunity\n\n## 📝 Plugin Examples\n\n### 1. Request Logger Plugin\n\n```typescript\nexport class RequestLoggerPlugin extends BasePlugin {\n  private logFile: string;\n\n  constructor(options: PluginOptions & { logFile?: string }) {\n    super({ name: 'request-logger', ...options });\n    this.logFile = options.logFile || 'requests.log';\n  }\n\n  async onPageCreated(page: Page): Promise<void> {\n    await page.setRequestInterception(true);\n    \n    page.on('request', (request) => {\n      const logEntry = {\n        timestamp: new Date().toISOString(),\n        method: request.method(),\n        url: request.url(),\n        headers: request.headers()\n      };\n      \n      // Log to file or console\n      console.log('Request:', logEntry);\n      request.continue();\n    });\n  }\n}\n```\n\n### 2. Ad Blocker Plugin\n\n```typescript\nexport class AdBlockerPlugin extends BasePlugin {\n  private blockedDomains: Set<string>;\n  private blockedCount: number = 0;\n\n  constructor(options: PluginOptions & { blockedDomains?: string[] }) {\n    super({ name: 'ad-blocker', ...options });\n    this.blockedDomains = new Set(options.blockedDomains || [\n      'doubleclick.net',\n      'googleadservices.com',\n      'googlesyndication.com'\n    ]);\n  }\n\n  async onPageCreated(page: Page): Promise<void> {\n    await page.setRequestInterception(true);\n    \n    page.on('request', (request) => {\n      const url = new URL(request.url());\n      \n      if (this.blockedDomains.has(url.hostname)) {\n        this.blockedCount++;\n        console.log(`Blocked ad request: ${url.hostname}`);\n        request.abort();\n      } else {\n        request.continue();\n      }\n    });\n  }\n\n  async onShutdown(): Promise<void> {\n    console.log(`Ad Blocker: Blocked ${this.blockedCount} requests`);\n  }\n}\n```\n\n### 3. Screenshot Plugin\n\n```typescript\nexport class ScreenshotPlugin extends BasePlugin {\n  private screenshotDir: string;\n\n  constructor(options: PluginOptions & { screenshotDir?: string }) {\n    super({ name: 'screenshot', ...options });\n    this.screenshotDir = options.screenshotDir || './screenshots';\n  }\n\n  async onPageNavigate(page: Page): Promise<void> {\n    // Take screenshot after navigation\n    setTimeout(async () => {\n      try {\n        const url = new URL(page.url());\n        const filename = `${url.hostname}-${Date.now()}.png`;\n        const filepath = path.join(this.screenshotDir, filename);\n        \n        await page.screenshot({ path: filepath, fullPage: true });\n        console.log(`Screenshot saved: ${filepath}`);\n      } catch (error) {\n        console.error('Screenshot failed:', error);\n      }\n    }, 2000);\n  }\n}\n```\n\n### 4. Performance Monitor Plugin\n\n```typescript\nexport class PerformancePlugin extends BasePlugin {\n  private metrics: Map<string, any> = new Map();\n\n  constructor(options: PluginOptions) {\n    super({ name: 'performance-monitor', ...options });\n  }\n\n  async onPageCreated(page: Page): Promise<void> {\n    // Enable performance monitoring\n    await page.coverage.startJSCoverage();\n    await page.coverage.startCSSCoverage();\n    \n    page.on('load', async () => {\n      const performanceMetrics = await page.evaluate(() => {\n        const perfData = performance.getEntriesByType('navigation')[0];\n        return {\n          domContentLoaded: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,\n          loadComplete: perfData.loadEventEnd - perfData.loadEventStart,\n          firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0\n        };\n      });\n      \n      this.metrics.set(page.url(), performanceMetrics);\n      console.log(`Performance metrics for ${page.url()}:`, performanceMetrics);\n    });\n  }\n\n  async onShutdown(): Promise<void> {\n    console.log('Performance Summary:', Object.fromEntries(this.metrics));\n  }\n}\n```\n\n## 🔧 Advanced Plugin Development\n\n### Accessing CDP Service\n\n```typescript\nexport class AdvancedPlugin extends BasePlugin {\n  async onBrowserLaunch(browser: Browser): Promise<void> {\n    // Access the CDP service\n    if (this.cdpService) {\n      // Get all pages\n      const pages = await this.cdpService.getPages();\n      \n      // Access primary page\n      const primaryPage = this.cdpService.primaryPage;\n      \n      // Register hooks\n      this.cdpService.registerLaunchHook(async (config) => {\n        console.log('Browser launching with config:', config);\n      });\n    }\n  }\n}\n```\n\n### Plugin Configuration\n\n```typescript\ninterface MyPluginOptions extends PluginOptions {\n  apiKey?: string;\n  endpoint?: string;\n  retries?: number;\n  timeout?: number;\n}\n\nexport class ConfigurablePlugin extends BasePlugin {\n  private config: MyPluginOptions;\n\n  constructor(options: MyPluginOptions) {\n    super(options);\n    this.config = {\n      apiKey: options.apiKey || process.env.API_KEY,\n      endpoint: options.endpoint || 'https://api.example.com',\n      retries: options.retries || 3,\n      timeout: options.timeout || 5000,\n      ...options\n    };\n  }\n}\n```\n\n### Error Handling\n\n```typescript\nexport class RobustPlugin extends BasePlugin {\n  async onPageCreated(page: Page): Promise<void> {\n    try {\n      // Plugin logic here\n      await this.setupPageInterception(page);\n    } catch (error) {\n      console.error(`Error in ${this.name} plugin:`, error);\n      // Plugin errors are isolated by the PluginManager\n      // but you should handle them gracefully\n    }\n  }\n\n  private async setupPageInterception(page: Page): Promise<void> {\n    // Implementation with proper error handling\n  }\n}\n```\n\n## 🧪 Testing Plugins\n\n### Unit Testing\n\n```typescript\n// plugin.test.ts\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { MyPlugin } from './my-plugin';\n\ndescribe('MyPlugin', () => {\n  let plugin: MyPlugin;\n\n  beforeEach(() => {\n    plugin = new MyPlugin({ name: 'test-plugin' });\n  });\n\n  it('should initialize with correct name', () => {\n    expect(plugin.name).toBe('test-plugin');\n  });\n\n  it('should handle browser launch', async () => {\n    const mockBrowser = {} as any; // Mock browser object\n    await expect(plugin.onBrowserLaunch(mockBrowser)).resolves.not.toThrow();\n  });\n});\n```\n\n### Integration Testing\n\n```typescript\n// integration.test.ts\nimport { CDPService } from '@steel-browser/api';\nimport { MyPlugin } from './my-plugin';\n\ndescribe('Plugin Integration', () => {\n  let cdpService: CDPService;\n  let plugin: MyPlugin;\n\n  beforeEach(async () => {\n    cdpService = new CDPService({}, console);\n    plugin = new MyPlugin({ name: 'test-plugin' });\n    cdpService.registerPlugin(plugin);\n  });\n\n  afterEach(async () => {\n    await cdpService.shutdown();\n  });\n\n  it('should work with CDP service', async () => {\n    await cdpService.launch();\n    // Test plugin behavior\n  });\n});\n```\n\n## 📦 Plugin Distribution\n\n### NPM Package Structure\n\n```\nmy-steel-plugin/\n├── src/\n│   ├── index.ts\n│   └── plugin.ts\n├── dist/\n├── package.json\n├── README.md\n└── tsconfig.json\n```\n\n### package.json Example\n\n```json\n{\n  \"name\": \"steel-plugin-example\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Example plugin for Steel Browser\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"keywords\": [\"steel-browser\", \"plugin\", \"automation\"],\n  \"peerDependencies\": {\n    \"@steel-browser/api\": \"^1.0.0\",\n    \"puppeteer-core\": \"^23.0.0\"\n  },\n  \"files\": [\"dist/**/*\"]\n}\n```\n\n### TypeScript Configuration\n\n```json\n{\n  \"extends\": \"@steel-browser/api/tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"declaration\": true,\n    \"declarationMap\": true\n  },\n  \"include\": [\"src/**/*\"]\n}\n```\n\n## 🌟 Best Practices\n\n### 1. Error Handling\n- Always wrap plugin logic in try-catch blocks\n- Log errors with context information\n- Don't let plugin errors crash the system\n\n### 2. Resource Management\n- Clean up resources in shutdown hooks\n- Remove event listeners when done\n- Close files and connections properly\n\n### 3. Performance\n- Avoid blocking operations in event handlers\n- Use async/await properly\n- Consider memory usage for long-running plugins\n\n### 4. Configuration\n- Provide sensible defaults\n- Support environment variables\n- Validate configuration options\n\n### 5. Testing\n- Write unit tests for plugin logic\n- Test with real browser instances\n- Mock external dependencies\n\n### 6. Documentation\n- Document plugin options and usage\n- Provide examples\n- Include troubleshooting guides\n\n## 🔍 Debugging Plugins\n\n### Enable Debug Logging\n\n```bash\n# Enable verbose logging\nENABLE_VERBOSE_LOGGING=true npm run dev -w api\n\n# Enable CDP logging\nENABLE_CDP_LOGGING=true npm run dev -w api\n```\n\n### Debug in VS Code\n\n```json\n// .vscode/launch.json\n{\n  \"configurations\": [\n    {\n      \"name\": \"Debug Steel with Plugin\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"program\": \"${workspaceFolder}/api/build/index.js\",\n      \"env\": {\n        \"NODE_ENV\": \"development\",\n        \"ENABLE_VERBOSE_LOGGING\": \"true\"\n      }\n    }\n  ]\n}\n```\n\n### Plugin Debug Helper\n\n```typescript\nexport class DebugPlugin extends BasePlugin {\n  private debug: boolean;\n\n  constructor(options: PluginOptions & { debug?: boolean }) {\n    super(options);\n    this.debug = options.debug || false;\n  }\n\n  private log(...args: any[]): void {\n    if (this.debug) {\n      console.log(`[${this.name}]`, ...args);\n    }\n  }\n\n  async onPageCreated(page: Page): Promise<void> {\n    this.log('Page created:', page.url());\n    // Plugin logic...\n  }\n}\n```\n\n## 📚 Plugin Registry (Future)\n\nWe're planning a plugin registry where you can:\n\n- Publish plugins for community use\n- Discover existing plugins\n- Rate and review plugins\n- Automatic plugin updates\n\nStay tuned for updates on this feature!\n\n## 🤝 Contributing Plugins\n\nTo contribute a plugin to the Steel Browser ecosystem:\n\n1. Create a well-documented plugin\n2. Add comprehensive tests\n3. Submit to the community registry\n4. Engage with users for feedback\n\n---\n\nHappy plugin development! 🚀 "
  },
  {
    "path": "docs/README.md",
    "content": "# Steel Browser Documentation\n\nWelcome to the Steel Browser documentation! This directory contains comprehensive guides and references to help you understand, use, and contribute to Steel Browser.\n\n## 📚 Documentation Overview\n\n### Getting Started\n\n- **[Development Setup Guide](DEVELOPMENT_SETUP.md)** - Complete setup instructions for development environment\n- **[Contributing Guide](../CONTRIBUTING.md)** - How to contribute to the project\n- **[Troubleshooting Guide](TROUBLESHOOTING.md)** - Common issues and solutions\n\n### Architecture & Design\n\n- **[Architecture Overview](ARCHITECTURE.md)** - System design and component relationships\n- **[Plugin Development Guide](PLUGIN_DEVELOPMENT.md)** - Creating custom plugins\n\n### API Reference\n\n- **[API Documentation](http://localhost:3000/documentation)** - Interactive API reference (when running locally)\n- **[OpenAPI Schema](../api/openapi/schemas.json)** - Machine-readable API specification\n\n## 🚀 Quick Links\n\n### For New Contributors\n\n1. Start with the [Contributing Guide](../CONTRIBUTING.md)\n2. Set up your development environment using the [Development Setup Guide](DEVELOPMENT_SETUP.md)\n3. Read the [Architecture Overview](ARCHITECTURE.md) to understand the system\n4. Check out issues labeled [`good first issue`](https://github.com/steel-dev/steel-browser/labels/good%20first%20issue)\n\n### For Plugin Developers\n\n1. Read the [Plugin Development Guide](PLUGIN_DEVELOPMENT.md)\n2. Study the [Architecture Overview](ARCHITECTURE.md) for system understanding\n3. Browse existing plugins in `api/src/services/cdp/plugins/`\n4. Join our [Discord](https://discord.gg/steel-dev) for plugin development discussions\n\n### For Users\n\n1. Check the main [README](../README.md) for basic usage\n2. Browse the [Steel Cookbook](https://github.com/steel-dev/steel-cookbook) for examples\n3. Use the [Troubleshooting Guide](TROUBLESHOOTING.md) if you encounter issues\n4. Visit the [API Documentation](http://localhost:3000/documentation) for detailed API reference\n\n## 🛠️ Documentation Structure\n\n```\ndocs/\n├── README.md                  # This file - documentation overview\n├── ARCHITECTURE.md           # System architecture and design\n├── DEVELOPMENT_SETUP.md      # Development environment setup\n├── PLUGIN_DEVELOPMENT.md     # Plugin creation guide\n└── TROUBLESHOOTING.md        # Common issues and solutions\n```\n\n## 📖 External Resources\n\n### Official Resources\n\n- **[Steel Browser Repository](https://github.com/steel-dev/steel-browser)** - Main repository\n- **[Steel Cookbook](https://github.com/steel-dev/steel-cookbook)** - Usage examples and recipes\n- **[Discord Community](https://discord.gg/steel-dev)** - Real-time support and discussions\n- **[Official Documentation](https://docs.steel.dev/)** - Comprehensive online docs\n\n### Learning Resources\n\n- **[Puppeteer Documentation](https://pptr.dev/)** - Browser automation library\n- **[Fastify Documentation](https://www.fastify.io/)** - Web framework used in API\n- **[React Documentation](https://react.dev/)** - Frontend framework\n- **[TypeScript Handbook](https://www.typescriptlang.org/docs/)** - TypeScript language guide\n\n## 🤝 Contributing to Documentation\n\nWe welcome contributions to improve our documentation! Here's how you can help:\n\n### Reporting Documentation Issues\n\n- **Missing Information**: If you can't find what you're looking for\n- **Outdated Content**: If documentation doesn't match current behavior\n- **Unclear Instructions**: If steps are confusing or incomplete\n- **Broken Links**: If links don't work or point to wrong resources\n\n### Improving Documentation\n\n1. **Fork the repository** and create a feature branch\n2. **Make your changes** to the relevant documentation files\n3. **Test your changes** by following the instructions you've written\n4. **Submit a pull request** with a clear description of improvements\n\n### Documentation Standards\n\n- **Use clear, concise language** that's accessible to all skill levels\n- **Include code examples** where applicable\n- **Add screenshots** for UI-related documentation\n- **Keep examples up-to-date** with current API and features\n- **Cross-reference related sections** to help users navigate\n\n## 🔍 Finding What You Need\n\n### By Use Case\n\n**I want to...**\n\n- **Use Steel Browser** → Start with the main [README](../README.md)\n- **Contribute code** → Read the [Contributing Guide](../CONTRIBUTING.md)\n- **Set up development** → Follow the [Development Setup Guide](DEVELOPMENT_SETUP.md)\n- **Create a plugin** → Study the [Plugin Development Guide](PLUGIN_DEVELOPMENT.md)\n- **Understand the system** → Read the [Architecture Overview](ARCHITECTURE.md)\n- **Fix an issue** → Check the [Troubleshooting Guide](TROUBLESHOOTING.md)\n\n### By Component\n\n**I'm working with...**\n\n- **API/Backend** → [Architecture](ARCHITECTURE.md) + [API Docs](http://localhost:3000/documentation)\n- **Frontend/UI** → [Architecture](ARCHITECTURE.md) + UI source code\n- **Plugins** → [Plugin Development Guide](PLUGIN_DEVELOPMENT.md)\n- **Docker** → [Development Setup](DEVELOPMENT_SETUP.md) + [Troubleshooting](TROUBLESHOOTING.md)\n\n## 📋 Documentation Roadmap\n\n### Planned Additions\n\n- **Deployment Guide** - Production deployment instructions\n- **Security Guide** - Security best practices and configuration\n- **Performance Guide** - Optimization and scaling recommendations\n- **Testing Guide** - Comprehensive testing strategies\n- **Migration Guide** - Upgrading between versions\n- **FAQ** - Frequently asked questions\n\n### Community Contributions Needed\n\n- **Platform-specific guides** (Windows, macOS, Linux variations)\n- **Integration examples** with popular tools and frameworks\n- **Video tutorials** for complex setup procedures\n- **Translated documentation** for non-English speakers\n- **Real-world use case studies**\n\n## 💡 Getting Help\n\nIf you can't find what you're looking for in the documentation:\n\n1. **Search existing issues** on GitHub\n2. **Ask in Discord** for real-time help\n3. **Create a documentation issue** describing what's missing\n4. **Check the [Steel Cookbook](https://github.com/steel-dev/steel-cookbook)** for practical examples\n\n## 🎯 Documentation Goals\n\nOur documentation aims to be:\n\n- **Comprehensive** - Covering all aspects of Steel Browser\n- **Accessible** - Understandable by users of all skill levels\n- **Up-to-date** - Reflecting the current state of the project\n- **Practical** - Including real-world examples and use cases\n- **Community-driven** - Improved through user feedback and contributions\n\n---\n\n**Happy learning and building with Steel Browser!** 🚀\n\n*Last updated: [Current Date] - If you notice outdated information, please let us know!* "
  },
  {
    "path": "docs/TROUBLESHOOTING.md",
    "content": "# Troubleshooting Guide\n\nThis guide helps you diagnose and resolve common issues with Steel Browser.\n\n## 🚀 Quick Diagnostics\n\n### Health Check Commands\n\n```bash\n# Check if services are running\ncurl http://localhost:3000/v1/health\n\n# Check API documentation\ncurl http://localhost:3000/documentation\n\n# Test basic functionality\ncd repl && npm start\n```\n\n### Environment Verification\n\n```bash\n# Check Node.js version (should be 22+)\nnode --version\n\n# Check npm version\nnpm --version\n\n# Check Chrome/Chromium\ngoogle-chrome --version\n# or\nchromium --version\n\n# Check Docker (if using containers)\ndocker --version\ndocker-compose --version\n```\n\n## 🔧 Common Issues\n\n### 1. Browser Launch Failures\n\n#### Symptoms\n- \"Failed to launch browser\" errors\n- Chrome executable not found\n- Permission denied errors\n\n#### Solutions\n\n**Chrome Not Found:**\n```bash\n# Set Chrome executable path\nexport CHROME_EXECUTABLE_PATH=/usr/bin/google-chrome\n# or for macOS\nexport CHROME_EXECUTABLE_PATH=\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"\n# or for Windows\nset CHROME_EXECUTABLE_PATH=\"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe\"\n```\n\n**Permission Issues (Linux):**\n```bash\n# Add user to necessary groups\nsudo usermod -a -G audio,video $USER\n\n# Install required dependencies\nsudo apt-get update\nsudo apt-get install -y \\\n  libnss3-dev \\\n  libatk-bridge2.0-dev \\\n  libdrm-dev \\\n  libxcomposite-dev \\\n  libxdamage-dev \\\n  libxrandr-dev \\\n  libgbm-dev \\\n  libxss-dev \\\n  libasound2-dev\n```\n\n**Headless Mode Issues:**\n```bash\n# Disable headless mode for debugging\nexport CHROME_HEADLESS=false\n\n# Or run with virtual display\nexport DISPLAY=:99\nXvfb :99 -screen 0 1024x768x24 &\n```\n\n### 2. Port Conflicts\n\n#### Symptoms\n- \"Port already in use\" errors\n- Cannot connect to API\n- Services fail to start\n\n#### Solutions\n\n```bash\n# Check what's using the ports\nlsof -i :3000  # API port\nlsof -i :5173  # UI port\nlsof -i :9223  # CDP port\n\n# Kill processes using the ports\nkill -9 $(lsof -t -i:3000)\n\n# Use different ports\nexport PORT=3001\nexport CDP_REDIRECT_PORT=9224\n```\n\n### 3. Memory Issues\n\n#### Symptoms\n- Browser crashes\n- \"Out of memory\" errors\n- Slow performance\n\n#### Solutions\n\n```bash\n# Increase Node.js memory limit\nexport NODE_OPTIONS=\"--max-old-space-size=4096\"\n\n# Monitor memory usage\ndocker stats  # if using Docker\nhtop          # system monitor\n\n# Reduce concurrent sessions\n# Limit browser instances in your code\n```\n\n### 4. Network/Proxy Issues\n\n#### Symptoms\n- Cannot reach external websites\n- Proxy authentication failures\n- SSL/TLS errors\n\n#### Solutions\n\n```bash\n# Set proxy configuration\nexport PROXY_URL=\"http://proxy.company.com:8080\"\nexport PROXY_URL=\"http://username:password@proxy.company.com:8080\"\n\n# Disable SSL verification (development only)\nexport NODE_TLS_REJECT_UNAUTHORIZED=0\n\n# Check network connectivity\ncurl -I https://google.com\n```\n\n### 5. File Permission Issues\n\n#### Symptoms\n- Cannot write files\n- Session storage errors\n- Extension loading failures\n\n#### Solutions\n\n```bash\n# Fix file permissions\nchmod -R 755 ./files\nchmod -R 755 ./.cache\n\n# Check disk space\ndf -h\n\n# Create required directories\nmkdir -p ./files\nmkdir -p ./.cache\nmkdir -p ./api/extensions/recorder/dist\n```\n\n### 6. Docker Issues\n\n#### Symptoms\n- Container fails to start\n- Cannot access services\n- Build failures\n\n#### Solutions\n\n```bash\n# Clean Docker cache\ndocker system prune -a\n\n# Rebuild without cache\ndocker-compose build --no-cache\n\n# Check container logs\ndocker-compose logs api\ndocker-compose logs ui\n\n# Fix volume permissions\nsudo chown -R $USER:$USER ./.cache\n```\n\n## 🐛 Debugging Techniques\n\n### 1. Enable Debug Logging\n\n```bash\n# Enable all debug logging\nexport NODE_ENV=development\nexport ENABLE_VERBOSE_LOGGING=true\nexport ENABLE_CDP_LOGGING=true\nexport LOG_CUSTOM_EMIT_EVENTS=true\n\n# Start with debug logging\nnpm run dev -w api\n```\n\n### 2. Chrome DevTools Debugging\n\n```bash\n# Start API with inspector\nnode --inspect ./api/build/index.js\n\n# Or with specific port\nnode --inspect=0.0.0.0:9229 ./api/build/index.js\n\n# Then open Chrome and go to:\n# chrome://inspect\n```\n\n### 3. Browser Debugging\n\n```bash\n# Run Chrome in non-headless mode\nexport CHROME_HEADLESS=false\n\n# Enable Chrome debugging\nexport DEBUG_CHROME_PROCESS=true\n\n# Connect to browser DevTools\n# Open http://localhost:9223 in your browser\n```\n\n### 4. Network Debugging\n\n```bash\n# Monitor network requests\nexport ENABLE_CDP_LOGGING=true\n\n# Use network debugging tools\ntcpdump -i any port 3000\nwireshark  # GUI network analyzer\n```\n\n### 5. Performance Debugging\n\n```bash\n# Enable performance monitoring\nnode --prof ./api/build/index.js\n\n# Generate performance report\nnode --prof-process isolate-*.log > performance.txt\n\n# Memory profiling\nnode --inspect --expose-gc ./api/build/index.js\n```\n\n## 📊 Log Analysis\n\n### Understanding Log Levels\n\n```\nERROR - Critical issues requiring immediate attention\nWARN  - Potential issues that might cause problems\nINFO  - General information about system operation\nDEBUG - Detailed information for troubleshooting\nTRACE - Very detailed execution information\n```\n\n### Common Log Patterns\n\n**Successful Session Creation:**\n```\nINFO: Session created successfully {sessionId: \"abc123\"}\nINFO: Browser launched {pid: 12345}\nINFO: Primary page created {url: \"about:blank\"}\n```\n\n**Connection Issues:**\n```\nERROR: Failed to connect to Chrome {error: \"ECONNREFUSED\"}\nWARN: Retrying browser launch {attempt: 2}\n```\n\n**Memory Warnings:**\n```\nWARN: High memory usage detected {usage: \"85%\"}\nINFO: Garbage collection triggered\n```\n\n## 🔍 Diagnostic Commands\n\n### System Information\n\n```bash\n# Get system info\nuname -a                    # System information\nfree -h                     # Memory usage\ndf -h                       # Disk usage\nps aux | grep chrome        # Chrome processes\nps aux | grep node          # Node processes\n```\n\n### Steel Browser Specific\n\n```bash\n# Check API health\ncurl -s http://localhost:3000/v1/health | jq\n\n# List active sessions\ncurl -s http://localhost:3000/v1/sessions | jq\n\n# Get session details\ncurl -s http://localhost:3000/v1/sessions/SESSION_ID | jq\n\n# Check file service\nls -la ./files/\n\n# Check cache\nls -la ./.cache/\n```\n\n### Docker Diagnostics\n\n```bash\n# Container status\ndocker-compose ps\n\n# Container logs\ndocker-compose logs --tail=50 api\ndocker-compose logs --tail=50 ui\n\n# Container resource usage\ndocker stats\n\n# Network information\ndocker network ls\ndocker network inspect steel-network\n```\n\n## 🚨 Error Codes\n\n### HTTP Status Codes\n\n- **400 Bad Request**: Invalid request parameters\n- **404 Not Found**: Session or resource not found\n- **408 Request Timeout**: Operation timed out\n- **409 Conflict**: Resource conflict (e.g., session already exists)\n- **500 Internal Server Error**: Server-side error\n- **503 Service Unavailable**: Browser not available\n\n### Custom Error Codes\n\n- **BROWSER_LAUNCH_FAILED**: Cannot start browser process\n- **SESSION_NOT_FOUND**: Session ID doesn't exist\n- **PAGE_LOAD_TIMEOUT**: Page failed to load within timeout\n- **CHROME_EXECUTABLE_NOT_FOUND**: Chrome binary not found\n- **INSUFFICIENT_MEMORY**: Not enough memory to start browser\n\n## 🛠️ Recovery Procedures\n\n### 1. Restart Services\n\n```bash\n# Graceful restart\nnpm run dev  # Ctrl+C then restart\n\n# Force restart\npkill -f \"node.*steel\"\nnpm run dev\n\n# Docker restart\ndocker-compose restart\n```\n\n### 2. Clear Cache and Temp Files\n\n```bash\n# Clear application cache\nrm -rf ./.cache/*\nrm -rf ./files/*\n\n# Clear npm cache\nnpm cache clean --force\n\n# Clear Docker cache\ndocker system prune -f\n```\n\n### 3. Reset to Clean State\n\n```bash\n# Stop all services\ndocker-compose down\n\n# Remove volumes\ndocker-compose down -v\n\n# Rebuild everything\ndocker-compose build --no-cache\ndocker-compose up\n```\n\n### 4. Database/Storage Recovery\n\n```bash\n# Clear session storage\nrm -rf ./db/data/*\n\n# Reset file storage\nrm -rf ./files/*\nmkdir -p ./files\n\n# Fix permissions\nchmod -R 755 ./files\nchmod -R 755 ./.cache\n```\n\n## 📞 Getting Help\n\n### Before Asking for Help\n\n1. **Check this troubleshooting guide**\n2. **Search existing GitHub issues**\n3. **Enable debug logging and collect logs**\n4. **Try the basic recovery procedures**\n5. **Prepare a minimal reproduction case**\n\n### Information to Include\n\nWhen reporting issues, include:\n\n```bash\n# System information\nuname -a\nnode --version\nnpm --version\ngoogle-chrome --version\n\n# Steel Browser logs (last 50 lines)\ntail -50 steel-browser.log\n\n# Configuration\nenv | grep -E \"(CHROME|PORT|HOST|NODE)\"\n\n# Steps to reproduce\n1. Start Steel Browser\n2. Create session with X configuration\n3. Navigate to Y URL\n4. Error occurs\n```\n\n### Support Channels\n\n- **GitHub Issues**: Bug reports and feature requests\n- **Discord**: Real-time community support\n- **Documentation**: Comprehensive guides and API reference\n- **Stack Overflow**: Tag questions with `steel-browser`\n\n### Creating Good Bug Reports\n\n```markdown\n## Bug Description\nClear description of what's wrong\n\n## Steps to Reproduce\n1. Step one\n2. Step two\n3. Error occurs\n\n## Expected Behavior\nWhat should happen\n\n## Actual Behavior\nWhat actually happens\n\n## Environment\n- OS: Ubuntu 20.04\n- Node: v22.0.0\n- Steel Browser: v1.0.0\n- Chrome: 91.0.4472.124\n\n## Logs\n```\nInclude relevant log output\n```\n\n## Additional Context\nAny other relevant information\n```\n\n## 🔧 Advanced Troubleshooting\n\n### Core Dumps\n\n```bash\n# Enable core dumps\nulimit -c unlimited\n\n# Analyze core dump\ngdb node core.12345\n```\n\n### Memory Leaks\n\n```bash\n# Use heap profiler\nnode --inspect --expose-gc ./api/build/index.js\n\n# Take heap snapshots\nkill -USR2 <node_pid>\n```\n\n### Network Issues\n\n```bash\n# Test network connectivity\nping google.com\nnslookup google.com\ntraceroute google.com\n\n# Check proxy settings\necho $HTTP_PROXY\necho $HTTPS_PROXY\necho $NO_PROXY\n```\n\n---\n\nStill having issues? Don't hesitate to reach out to our community for help! 🤝 "
  },
  {
    "path": "nginx.conf",
    "content": "events {\n    worker_connections 1024;\n}\n\nhttp {\n    server {\n        listen 9223;\n        \n        location / {\n            proxy_pass http://127.0.0.1:9222;\n            proxy_http_version 1.1;\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection \"upgrade\";\n            proxy_set_header Host $host;\n        }\n    }\n} "
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"steel-browser\",\n  \"version\": \"0.5.1\",\n  \"private\": true,\n  \"license\": \"Apache-2.0\",\n  \"author\": \"\",\n  \"type\": \"module\",\n  \"workspaces\": [\n    \"api\",\n    \"ui\",\n    \"repl\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build -w api -w ui\",\n    \"dev\": \"concurrently \\\"npm run dev -w api\\\" \\\"npm run dev -w ui\\\"\",\n    \"prepare\": \"husky\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"^19.7.1\",\n    \"@commitlint/config-conventional\": \"^19.7.1\",\n    \"@types/archiver\": \"^6.0.3\",\n    \"@types/turndown\": \"^5.0.5\",\n    \"concurrently\": \"^8.2.0\",\n    \"tsx\": \"^4.19.2\",\n    \"typescript\": \"^5.7.3\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  },\n  \"dependencies\": {\n    \"@joplin/turndown-plugin-gfm\": \"^1.0.62\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"fastify\": \"^5.2.1\",\n    \"husky\": \"^9.1.7\",\n    \"lodash-es\": \"^4.17.21\"\n  },\n  \"overrides\": {\n    \"tar-fs\": \">=3.1.1\"\n  }\n}\n"
  },
  {
    "path": "render.yaml",
    "content": "services:\n  - type: web\n    runtime: docker\n    name: steel-browser\n    dockerfilePath: ./Dockerfile\n    healthCheckPath: /v1/health\n    envVars:\n      - key: NODE_ENV\n        value: production\n      - key: DOMAIN\n        value: \"<change me to your render domain>\"\n      - key: USE_SSL\n        value: \"true\"\n    disk:\n      name: cache\n      mountPath: /app/.cache\n      sizeGB: 1\n"
  },
  {
    "path": "repl/README.md",
    "content": "# Steel REPL\n\nThis package provides a simple REPL to interact with the browser instance you've created using the API.\n\nThe API exposes a WebSocket endpoint, allowing you to connect to the browser using Chrome DevTools Protocol (CDP) and use Puppeteer as usual.\n\n## Quick Start\n\n1. Ensure you have **Steel Browser** running, either via Docker or locally.\n2. Run `npm start` to execute the script.\n3. Modify `src/script.ts` as needed and rerun `npm start` to see your changes.\n\n> Note: You might need to update the WebSocket endpoint in `src/script.ts` if your services isn't exposed on your network\n\nFor more details, refer to [Steel Browser Documentation](https://docs.steel.dev/)."
  },
  {
    "path": "repl/package.json",
    "content": "{\n  \"name\": \"@steel-browser/repl\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"tsx ./src/script.ts\"\n  },\n  \"dependencies\": {\n    \"puppeteer-core\": \"^24.2.0\"\n  },\n  \"devDependencies\": {\n    \"tsx\": \"*\"\n  },\n  \"engines\": {\n    \"node\": \">=22.0.0\"\n  },\n  \"overrides\": {\n    \"tar-fs\": \">=3.1.1\"\n  }\n}"
  },
  {
    "path": "repl/src/script.ts",
    "content": "import puppeteer from \"puppeteer-core\";\n\nasync function run() {\n  // WebSocket endpoint to connect Browser using Chrome DevTools Protocol (CDP)\n  const wsEndpoint = \"ws://0.0.0.0:3000\";\n  const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });\n  \n  try {\n    const page = await browser.newPage();\n\n    // Navigate to a website and log the title\n    await page.goto(\"https://steel.dev\");\n\n    console.log(`Page title: ${await page.title()}`);\n  } finally {\n    // Cleanup: close all pages and disconnect browser\n    await Promise.all((await browser.pages()).map((p) => p.close()));\n    await browser.disconnect();  \n  }\n}\n\nrun().catch(console.error);\n"
  },
  {
    "path": "ui/.dockerignore",
    "content": "node_modules/\n.env.local\n.env\n"
  },
  {
    "path": "ui/.eslintrc.cjs",
    "content": "module.exports = {\n  root: true,\n  env: { browser: true, es2020: true },\n  extends: [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/recommended\",\n    \"plugin:react-hooks/recommended\",\n  ],\n  ignorePatterns: [\"dist\", \".eslintrc.cjs\"],\n  parser: \"@typescript-eslint/parser\",\n  plugins: [\"react-refresh\"],\n  rules: {\n    \"react-refresh/only-export-components\": [\n      \"warn\",\n      { allowConstantExport: true },\n    ],\n    \"react-hooks/exhaustive-deps\": \"off\",\n    \"@typescript-eslint/no-explicit-any\": \"off\",\n  },\n};\n"
  },
  {
    "path": "ui/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n.env.local\n"
  },
  {
    "path": "ui/Dockerfile",
    "content": "ARG NODE_VERSION=22.13.0\n\nFROM node:${NODE_VERSION} AS base\n\nWORKDIR /app\n\nLABEL org.opencontainers.image.source=\"https://github.com/steel-dev/steel-browser\"\n\n# Copy package.json and package-lock.json first\nCOPY --link package.json package-lock.json ./\nCOPY --link ui/ ./ui/\n\n# Install the npm packages directly in the Docker container's working directory\nRUN npm ci --include=dev -w ui --ignore-scripts\n\n# Build the application\nRUN npm run build -w ui\n\n# Prune dev dependencies\nRUN npm prune --omit=dev -w ui\n\nFROM nginx:alpine\nCOPY --from=base /app/ui/dist /usr/share/nginx/html\nCOPY --from=base /app/ui/nginx.conf.template /etc/nginx/nginx.conf.template\nCOPY --chmod=755 --from=base /app/ui/entrypoint.sh /docker-entrypoint.sh\n\nEXPOSE 80\n\nENTRYPOINT [\"/docker-entrypoint.sh\"]\n"
  },
  {
    "path": "ui/README.md",
    "content": "# React + TypeScript + Vite\n\nThis template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.\n\nCurrently, two official plugins are available:\n\n- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh\n- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh\n\n## Expanding the ESLint configuration\n\nIf you are developing a production application, we recommend updating the configuration to enable type aware lint rules:\n\n- Configure the top-level `parserOptions` property like this:\n\n```js\n   parserOptions: {\n    ecmaVersion: 'latest',\n    sourceType: 'module',\n    project: ['./tsconfig.json', './tsconfig.node.json'],\n    tsconfigRootDir: dirname(fileURLToPath(import.meta.url)),\n   },\n```\n\n- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`\n- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`\n- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list\n"
  },
  {
    "path": "ui/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/index.css\",\n    \"baseColor\": \"zinc\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  }\n}"
  },
  {
    "path": "ui/entrypoint.sh",
    "content": "#!/bin/sh\nset -e\n\nlog() {\n    echo \"[$(date '+%Y-%m-%d %H:%M:%S')] $1\"\n}\n\nsubstitute_env_vars() {\n    log \"Substituting environment variables in nginx config template...\"\n    sed -e \"s|__API_URL__|${API_URL}|g\" /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf\n}\n\nmain() {\n    substitute_env_vars\n    log \"Starting nginx...\"\n    exec nginx -g 'daemon off;'\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "ui/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"/icon.png\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta property=\"og:title\" content=\"Steel API\" />\n    <meta\n      property=\"og:description\"\n      content=\"Steel is an open-source browser API purpose-built for AI agents. Control fleets of browser sessions in the cloud via API or Python/Node SDKs.\"\n    />\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:url\" content=\"https://steel.dev\" />\n    <meta\n      name=\"description\"\n      content=\"Steel is an open-source browser API purpose-built for AI agents, offering browser control, data extraction, and session management capabilities.\"\n    />\n    <title>Steel | Open-source Headless Browser API</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "ui/nginx.conf.template",
    "content": "worker_processes 1;\nevents { worker_connections 1024; }\n\nhttp {\n  include       mime.types;\n  default_type  application/octet-stream;\n  sendfile        on;\n\n  server {\n    listen 80;\n\n    location /api/ {\n      proxy_pass __API_URL__/;\n      proxy_set_header Host $host;\n      proxy_set_header X-Real-IP $remote_addr;\n    }\n\n    location /ws/ {\n      proxy_pass __API_URL__/;\n      # Required for WebSocket\n      proxy_http_version 1.1;\n      proxy_set_header Upgrade $http_upgrade;\n      proxy_set_header Connection \"upgrade\";\n      proxy_set_header Host $host;\n    }\n\n    location / {\n      root   /usr/share/nginx/html;\n      index  index.html;\n      try_files $uri $uri/ /index.html;\n    }\n  }\n}\n"
  },
  {
    "path": "ui/openapi-ts.config.ts",
    "content": "import { defineConfig } from \"@hey-api/openapi-ts\";\nimport dotenv from \"dotenv\";\n\ndotenv.config({ path: \".env.local\" });\n\nexport default defineConfig({\n  client: \"@hey-api/client-fetch\",\n  input: \"../api/openapi/schemas.json\",\n  output: {\n    format: \"prettier\",\n    path: \"./src/steel-client\",\n  },\n  types: {\n    dates: \"types+transform\",\n    enums: \"javascript\",\n  },\n});\n"
  },
  {
    "path": "ui/package.json",
    "content": "{\n  \"name\": \"@steel-browser/ui\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"serve dist -l 5173\",\n    \"dev\": \"vite --host ${HOST:-0.0.0.0}\",\n    \"build\": \"tsc && vite build\",\n    \"lint\": \"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 10\",\n    \"preview\": \"vite preview\",\n    \"generate-api\": \"openapi-ts\"\n  },\n  \"dependencies\": {\n    \"@fontsource/inter\": \"^5.0.13\",\n    \"@hey-api/client-fetch\": \"^0.4.0\",\n    \"@hookform/resolvers\": \"^3.9.0\",\n    \"@radix-ui/react-avatar\": \"^1.1.1\",\n    \"@radix-ui/react-checkbox\": \"^1.1.2\",\n    \"@radix-ui/react-dialog\": \"^1.1.2\",\n    \"@radix-ui/react-icons\": \"^1.3.0\",\n    \"@radix-ui/react-label\": \"^2.1.0\",\n    \"@radix-ui/react-popover\": \"^1.1.2\",\n    \"@radix-ui/react-select\": \"^2.1.2\",\n    \"@radix-ui/react-separator\": \"^1.1.0\",\n    \"@radix-ui/react-slot\": \"^1.1.0\",\n    \"@radix-ui/react-tabs\": \"^1.1.1\",\n    \"@radix-ui/react-toast\": \"^1.2.2\",\n    \"@radix-ui/themes\": \"^3.0.3\",\n    \"@tanstack/react-query\": \"^4.36.1\",\n    \"@tanstack/react-table\": \"^8.20.5\",\n    \"@vitejs/plugin-react\": \"^4.3.4\",\n    \"axios\": \"^1.12.0\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.1\",\n    \"i\": \"^0.3.7\",\n    \"jwt-decode\": \"^3.1.2\",\n    \"lucide-react\": \"^0.447.0\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-hook-form\": \"^7.53.0\",\n    \"react-router-dom\": \"^6.16.0\",\n    \"react-top-loading-bar\": \"^2.3.1\",\n    \"rrweb-player\": \"^1.0.0-alpha.4\",\n    \"serve\": \"^14.0.1\",\n    \"styled-components\": \"^6.0.8\",\n    \"tailwind-merge\": \"^2.5.2\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"ua-parser-js\": \"^1.0.39\",\n    \"usehooks-ts\": \"^3.1.0\",\n    \"zod\": \"^3.23.8\"\n  },\n  \"devDependencies\": {\n    \"@hey-api/openapi-ts\": \"^0.53.6\",\n    \"@swc/cli\": \"^0.6.0\",\n    \"@swc/core\": \"^1.10.18\",\n    \"@types/node\": \"^20.8.4\",\n    \"@types/react\": \"^18.2.15\",\n    \"@types/react-dom\": \"^18.2.7\",\n    \"@types/react-syntax-highlighter\": \"^15.5.8\",\n    \"@types/styled-components\": \"^5.1.28\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.0.0\",\n    \"@typescript-eslint/parser\": \"^6.0.0\",\n    \"@vitejs/plugin-react-swc\": \"^3.3.2\",\n    \"autoprefixer\": \"^10.4.20\",\n    \"eslint\": \"^8.45.0\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.3\",\n    \"postcss\": \"^8.4.47\",\n    \"tailwindcss\": \"^3.4.13\",\n    \"typescript\": \"^5.7.3\",\n    \"vite\": \"^6.3.5\"\n  },\n  \"overrides\": {\n    \"tar-fs\": \">=3.1.1\"\n  }\n}"
  },
  {
    "path": "ui/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "ui/src/App.tsx",
    "content": "import \"@fontsource/inter\";\nimport \"@radix-ui/themes/styles.css\";\nimport RootLayout from \"@/root-layout\";\nimport { client } from \"@/steel-client\";\nimport { env } from \"@/env\";\n\nclient.setConfig({\n  baseUrl: env.VITE_API_URL,\n});\n\nfunction App() {\n  return <RootLayout />;\n}\n\nexport default App;\n"
  },
  {
    "path": "ui/src/components/badges/proxy-badge.tsx",
    "content": "import { CopyIcon } from \"@radix-ui/react-icons\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { copyText } from \"@/utils/toasts\";\n\nexport function ProxyBadge({ proxy }: { proxy: string }) {\n  return (\n    <Badge\n      variant=\"secondary\"\n      className=\"text-[var(--gray-11)] bg-[var(--gray-a3)] gap-2 py-1 px-3 flex items-center justify-between max-w-fit\t\"\n    >\n      {proxy}\n      <CopyIcon\n        className=\"cursor-pointer text-[var(--gray-11)] hover:text-[var(--gray-12)]\"\n        width={16}\n        height={16}\n        onClick={() => copyText(proxy, \"Proxy IP\")}\n      />\n    </Badge>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/badges/user-agent-badge.tsx",
    "content": "import { DesktopIcon, CopyIcon } from \"@radix-ui/react-icons\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { ChromeIcon } from \"@/components/icons/ChromeIcon\";\nimport { copyText } from \"@/utils/toasts\";\nimport UAParser from \"ua-parser-js\";\nexport function UserAgentBadge({ userAgent }: { userAgent: string }) {\n  const parser = new UAParser(userAgent);\n\n  return (\n    <Badge\n      variant=\"secondary\"\n      className=\"text-[var(--gray-11)] bg-[var(--gray-a3)] gap-2 py-1 px-3 flex items-center justify-between max-w-fit\"\n    >\n      <DesktopIcon width={16} height={16} color=\"var(--gray-11)\" />{\" \"}\n      {parser.getDevice().type || \"Desktop\"}\n      <ChromeIcon width={16} height={16} color=\"var(--gray-11)\" />{\" \"}\n      {`${parser.getBrowser().name} (v${parser.getBrowser().version})`}\n      <CopyIcon\n        width={16}\n        height={16}\n        className=\"cursor-pointer text-[var(--gray-11)] hover:text-[var(--gray-12)]\"\n        onClick={() => copyText(userAgent, \"User Agent\")}\n      />\n    </Badge>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/badges/websocket-url-badge.tsx",
    "content": "import { CopyIcon } from \"@radix-ui/react-icons\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { copyText } from \"@/utils/toasts\";\n\nexport function WebsocketUrlBadge({ url }: { url: string }) {\n  return (\n    <Badge\n      variant=\"secondary\"\n      className=\"text-[var(--gray-11)] gap-2 py-1 px-3 flex items-center justify-between max-w-fit\t\"\n    >\n      {url}\n      <CopyIcon\n        className=\"cursor-pointer text-[var(--gray-11)] hover:text-[var(--gray-12)]\"\n        width={16}\n        height={16}\n        onClick={() => copyText(url, \"Websocket URL\")}\n      />\n    </Badge>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/header/header.tsx",
    "content": "import { ChevronRightIcon } from \"@radix-ui/react-icons\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { GlowingGreenDot } from \"@/components/icons/GlowingGreenDot\";\nimport { useSessionsContext } from \"@/hooks/use-sessions-context\";\nimport { SteelIcon } from \"../icons/SessionIcon\";\n\nexport const Header = () => {\n  const { pathname } = window.location;\n  const currentSessionId =\n    pathname.includes(\"sessions\") && pathname.split(\"/\").pop() !== \"sessions\"\n      ? pathname.split(\"/\").pop()\n      : null;\n\n  const { useSession } = useSessionsContext();\n  const { data: session, isLoading } = useSession(currentSessionId!);\n\n  return (\n    <header className=\"flex justify-between items-center pl-3 pr-10 h-20 w-full\">\n      <div className=\"flex-1\"></div>\n      <div className=\"flex items-center gap-2 text-muted-foreground\">\n        <div className=\"flex items-center gap-3\">\n          {currentSessionId ? (\n            <>\n              <SteelIcon />\n              Session\n            </>\n          ) : (\n            <>\n              <SteelIcon />\n              Session\n            </>\n          )}\n        </div>\n        {currentSessionId && (\n          <>\n            <ChevronRightIcon />\n            <div className=\"flex items-center gap-1.5 text-primary\">\n              <span className=\"text-sm font-mono\">\n                #{currentSessionId.split(\"-\")[0]}\n              </span>\n              {!isLoading && session?.status === \"live\" && (\n                <Badge\n                  variant=\"secondary\"\n                  className=\"text-[var(--green-a12)] border border-[var(--green-6)] bg-transparent gap-2 py-0.5 px-2.5 mb-0.5 flex items-center justify-between max-w-fit rounded-full\"\n                >\n                  <GlowingGreenDot />\n                  Live\n                </Badge>\n              )}\n            </div>\n          </>\n        )}\n      </div>\n      <nav className=\"flex-1 flex justify-end\">\n        <div className=\"flex gap-2 items-center\">\n          <a\n            href=\"https://docs.steel.dev\"\n            target=\"_blank\"\n            className=\"rounded-md opacity-90 bg-transparent flex h-10 px-4 justify-center items-center gap-3 text-primary hover:bg-[rgba(238,206,254,0.13)] font-inter text-base font-normal leading-6 cursor-pointer\"\n          >\n            Docs\n          </a>\n          <a\n            href=\"https://discord.gg/steel-dev\"\n            target=\"_blank\"\n            className=\"rounded-md opacity-90 bg-transparent flex h-10 px-4 justify-center items-center gap-3 text-primary hover:bg-[rgba(238,206,254,0.13)] font-inter text-base font-normal leading-6 cursor-pointer\"\n          >\n            Discord\n          </a>\n        </div>\n      </nav>\n    </header>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/header/index.tsx",
    "content": "export { Header } from \"./header\";\n"
  },
  {
    "path": "ui/src/components/icons/ChromeIcon.tsx",
    "content": "import { IconProps } from \"@/types/props\";\n\nexport function ChromeIcon({\n  width = 12,\n  height = 12,\n  color = \"currentColor\",\n}: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={width}\n      height={height}\n      viewBox=\"0 0 12 12\"\n      fill=\"none\"\n    >\n      <g clipPath=\"url(#clip0_2850_3925)\">\n        <path\n          d=\"M6.66675 11C9.42817 11 11.6667 8.76142 11.6667 6C11.6667 3.23858 9.42817 1 6.66675 1C3.90532 1 1.66675 3.23858 1.66675 6C1.66675 8.76142 3.90532 11 6.66675 11Z\"\n          stroke={color}\n          strokeWidth=\"1.25\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M6.66675 8C7.77132 8 8.66675 7.10457 8.66675 6C8.66675 4.89543 7.77132 4 6.66675 4C5.56218 4 4.66675 4.89543 4.66675 6C4.66675 7.10457 5.56218 8 6.66675 8Z\"\n          stroke={color}\n          strokeWidth=\"1.25\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M11.2517 4H6.66675\"\n          stroke={color}\n          strokeWidth=\"1.25\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M2.64172 3.03003L4.93672 7.00003\"\n          stroke={color}\n          strokeWidth=\"1.25\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M6.10669 10.97L8.39669 7\"\n          stroke={color}\n          strokeWidth=\"1.25\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_2850_3925\">\n          <rect\n            width={width}\n            height={height}\n            fill=\"white\"\n            transform=\"translate(0.666748)\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/icons/DeleteIcon.tsx",
    "content": "import { IconProps } from \"@/types/props\";\n\nexport function DeleteIcon({ width = 28, height = 28 }: IconProps) {\n  return (\n    <svg\n      width={width}\n      height={height}\n      viewBox=\"0 0 28 28\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M8 10H20\"\n        stroke=\"#CE2C31\"\n        strokeWidth=\"1.25\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M18.6663 10V19.3333C18.6663 20 17.9997 20.6667 17.333 20.6667H10.6663C9.99967 20.6667 9.33301 20 9.33301 19.3333V10\"\n        stroke=\"#CE2C31\"\n        strokeWidth=\"1.25\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M11.333 9.99992V8.66659C11.333 7.99992 11.9997 7.33325 12.6663 7.33325H15.333C15.9997 7.33325 16.6663 7.99992 16.6663 8.66659V9.99992\"\n        stroke=\"#CE2C31\"\n        strokeWidth=\"1.25\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M12.667 13.3333V17.3333\"\n        stroke=\"#CE2C31\"\n        strokeWidth=\"1.25\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M15.333 13.3333V17.3333\"\n        stroke=\"#CE2C31\"\n        strokeWidth=\"1.25\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/icons/GlobeIcon.tsx",
    "content": "export const GlobeIcon = () => {\n  return (\n    <svg\n      width=\"17\"\n      height=\"18\"\n      viewBox=\"0 0 17 18\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M8.5 17C12.9183 17 16.5 13.4183 16.5 9C16.5 4.58172 12.9183 1 8.5 1C4.08172 1 0.5 4.58172 0.5 9C0.5 13.4183 4.08172 17 8.5 17Z\"\n        stroke=\"#FFCA16\"\n        strokeMiterlimit=\"10\"\n      />\n      <path d=\"M8.5 1.01355V17\" stroke=\"#FFCA16\" strokeMiterlimit=\"10\" />\n      <path\n        d=\"M8.5 17C10.7091 17 12.5 13.4183 12.5 9C12.5 4.58172 10.7091 1 8.5 1C6.29086 1 4.5 4.58172 4.5 9C4.5 13.4183 6.29086 17 8.5 17Z\"\n        stroke=\"#FFCA16\"\n        strokeMiterlimit=\"10\"\n      />\n      <path d=\"M0.513672 9H16.5001\" stroke=\"#FFCA16\" strokeMiterlimit=\"10\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/icons/GlowingGreenDot.tsx",
    "content": "export const GlowingGreenDot = () => {\n  return (\n    <span className=\"relative flex h-2 w-2\">\n      <span className=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--green-11)] opacity-75\"></span>\n      <span className=\"relative inline-flex rounded-full h-2 w-2 bg-[var(--green-11)]\"></span>\n    </span>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/icons/KeyIcon.tsx",
    "content": "import { IconProps } from \"@/types/props\";\n\nexport function KeyIcon({\n  width = 12,\n  height = 12,\n  color = \"currentColor\",\n}: IconProps) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={width}\n      height={height}\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n    >\n      <path\n        d=\"M14 1.3335L12.6667 2.66683ZM7.59333 7.74016C7.93756 8.07981 8.2112 8.48419 8.3985 8.93002C8.5858 9.37586 8.68306 9.85434 8.68468 10.3379C8.68631 10.8215 8.59225 11.3006 8.40794 11.7477C8.22363 12.1948 7.95271 12.601 7.61076 12.9429C7.26882 13.2849 6.86261 13.5558 6.41554 13.7401C5.96846 13.9244 5.48933 14.0185 5.00575 14.0168C4.52218 14.0152 4.0437 13.918 3.59786 13.7307C3.15203 13.5434 2.74764 13.2697 2.408 12.9255C1.74009 12.234 1.37051 11.3077 1.37886 10.3464C1.38722 9.38497 1.77284 8.46532 2.45267 7.78549C3.13249 7.10566 4.05214 6.72005 5.01353 6.71169C5.97492 6.70334 6.90113 7.07292 7.59267 7.74083L7.59333 7.74016ZM7.59333 7.74016L10.3333 5.00016ZM10.3333 5.00016L12.3333 7.00016L14.6667 4.66683L12.6667 2.66683M10.3333 5.00016L12.6667 2.66683Z\"\n        fill=\"transparent\"\n      />\n      <path\n        d=\"M14 1.3335L12.6667 2.66683M12.6667 2.66683L14.6667 4.66683L12.3333 7.00016L10.3333 5.00016M12.6667 2.66683L10.3333 5.00016M7.59333 7.74016C7.93756 8.07981 8.2112 8.48419 8.3985 8.93002C8.5858 9.37586 8.68306 9.85434 8.68468 10.3379C8.68631 10.8215 8.59225 11.3006 8.40794 11.7477C8.22363 12.1948 7.95271 12.601 7.61076 12.9429C7.26882 13.2849 6.86261 13.5558 6.41554 13.7401C5.96846 13.9244 5.48933 14.0185 5.00575 14.0168C4.52218 14.0152 4.0437 13.918 3.59786 13.7307C3.15203 13.5434 2.74764 13.2697 2.408 12.9255C1.74009 12.234 1.37051 11.3077 1.37886 10.3464C1.38722 9.38497 1.77284 8.46532 2.45267 7.78549C3.13249 7.10566 4.05214 6.72005 5.01353 6.71169C5.97492 6.70334 6.90113 7.07292 7.59267 7.74083L7.59333 7.74016ZM7.59333 7.74016L10.3333 5.00016\"\n        stroke={color}\n        strokeWidth=\"1.25\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/icons/LoadingSpinner.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport interface LoadingSpinnerProps {\n  className?: string;\n}\n\nexport const LoadingSpinner = ({ className }: LoadingSpinnerProps) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"48\"\n      height=\"48\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={cn(\"animate-spin\", className)}\n    >\n      <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/icons/NinjaIcon.tsx",
    "content": "import { IconProps } from \"@/types/props\";\n\nexport function NinjaIcon({ color = \"#A1A1AA\" }: IconProps) {\n  return (\n    <svg\n      width=\"15\"\n      height=\"13\"\n      viewBox=\"0 0 15 13\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M6.10457 0.606812C9.4736 0.606812 12.2091 3.34236 12.2091 6.71138C12.2091 10.0804 9.4736 12.816 6.10457 12.816C2.73555 12.816 0 10.0804 0 6.71138C0 3.34236 2.73555 0.606812 6.10457 0.606812ZM6.10457 1.65331C3.31321 1.65331 1.0465 3.92002 1.0465 6.71138C1.0465 9.50274 3.31321 11.7695 6.10457 11.7695C8.89593 11.7695 11.1626 9.50274 11.1626 6.71138C11.1626 3.92002 8.89593 1.65331 6.10457 1.65331Z\"\n        fill={color}\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M13.698 0.70727V1.34145C13.698 1.98469 13.4427 2.60143 12.9878 3.05561C12.5336 3.51049 11.9162 3.76653 11.2736 3.76653H10.6395C10.3506 3.76653 10.1162 3.53212 10.1162 3.24328V2.60911C10.1162 1.96586 10.3716 1.34912 10.8264 0.894244C11.2813 0.439366 11.898 0.184021 12.5413 0.184021H13.1748C13.4643 0.184021 13.698 0.418437 13.698 0.70727ZM12.6515 1.23052H12.5413C12.1757 1.23052 11.8248 1.37563 11.5667 1.63447C11.3078 1.8926 11.1627 2.24353 11.1627 2.60911V2.72003H11.2736C11.6392 2.72003 11.9894 2.57422 12.2483 2.31609C12.5064 2.05725 12.6515 1.70702 12.6515 1.34145V1.23052Z\"\n        fill={color}\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M14.7306 5.46047L14.1766 5.76815C13.6143 6.0807 12.9515 6.15744 12.3327 5.98024C11.7145 5.80373 11.1913 5.38931 10.8787 4.82699L10.5711 4.27305C10.4308 4.02049 10.5215 3.70166 10.7741 3.56143L11.328 3.25306C11.8904 2.94051 12.5538 2.86446 13.172 3.04097C13.7908 3.21748 14.3133 3.63259 14.6259 4.19491L14.9343 4.74886C15.0745 5.00141 14.9831 5.32024 14.7306 5.46047ZM13.765 4.79979L13.7113 4.70281C13.5334 4.38328 13.2362 4.14747 12.8845 4.047C12.5329 3.94724 12.1562 3.99049 11.8366 4.1677L11.7397 4.22142L11.7934 4.3184C11.9713 4.63793 12.2685 4.87374 12.6201 4.9742C12.9717 5.07467 13.3485 5.03141 13.668 4.85351L13.765 4.79979Z\"\n        fill={color}\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M10.465 6.71136C10.465 8.05995 9.37172 9.15319 8.02313 9.15319H4.18597C2.83738 9.15319 1.74414 8.05995 1.74414 6.71136C1.74414 5.36277 2.83738 4.26953 4.18597 4.26953H8.02313C9.37172 4.26953 10.465 5.36277 10.465 6.71136ZM9.41846 6.71136C9.41846 5.94044 8.79405 5.31603 8.02313 5.31603H4.18597C3.41505 5.31603 2.79064 5.94044 2.79064 6.71136C2.79064 7.48228 3.41505 8.10669 4.18597 8.10669H8.02313C8.79405 8.10669 9.41846 7.48228 9.41846 6.71136Z\"\n        fill={color}\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M7.73368 7.35391C7.47554 7.48298 7.16089 7.37833 7.03182 7.1195C6.90276 6.86136 7.00741 6.54671 7.26624 6.41764L7.96391 6.06881C8.22204 5.93974 8.53669 6.04439 8.66576 6.30323C8.79482 6.56136 8.69017 6.87601 8.43134 7.00508L7.73368 7.35391Z\"\n        fill={color}\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M4.94282 6.41764C5.20165 6.54671 5.3063 6.86136 5.17723 7.1195C5.04816 7.37833 4.73352 7.48298 4.47538 7.35391L3.77771 7.00508C3.51888 6.87601 3.41423 6.56136 3.5433 6.30323C3.67237 6.04439 3.98701 5.93974 4.24515 6.06881L4.94282 6.41764Z\"\n        fill={color}\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/icons/SessionIcon.tsx",
    "content": "export const SteelIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n    >\n      <g clipPath=\"url(#clip0_5055_44)\">\n        <rect x=\"4\" width=\"4\" height=\"4\" fill=\"#F0DBFF\" fillOpacity=\"0.979\" />\n        <rect x=\"8\" width=\"4\" height=\"4\" fill=\"#F0DBFF\" fillOpacity=\"0.979\" />\n        <rect y=\"4\" width=\"4\" height=\"4\" fill=\"#F0DBFF\" fillOpacity=\"0.979\" />\n        <rect\n          x=\"4\"\n          y=\"12\"\n          width=\"4\"\n          height=\"4\"\n          fill=\"#F0DBFF\"\n          fillOpacity=\"0.979\"\n        />\n        <rect\n          x=\"12\"\n          y=\"4\"\n          width=\"4\"\n          height=\"4\"\n          fill=\"#F0DBFF\"\n          fillOpacity=\"0.979\"\n        />\n        <rect y=\"8\" width=\"4\" height=\"4\" fill=\"#F0DBFF\" fillOpacity=\"0.979\" />\n        <rect\n          x=\"8\"\n          y=\"12\"\n          width=\"4\"\n          height=\"4\"\n          fill=\"#F0DBFF\"\n          fillOpacity=\"0.979\"\n        />\n        <rect\n          x=\"12\"\n          y=\"8\"\n          width=\"4\"\n          height=\"4\"\n          fill=\"#F0DBFF\"\n          fillOpacity=\"0.979\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_5055_44\">\n          <rect width=\"16\" height=\"16\" fill=\"white\" />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/icons/SettingsIcon.tsx",
    "content": "export const SettingsIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"17\"\n      height=\"16\"\n      viewBox=\"0 0 17 16\"\n      fill=\"none\"\n    >\n      <g clipPath=\"url(#clip0_5055_2832)\">\n        <rect x=\"0.5\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"4.5\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"4.5\" y=\"4\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"0.5\" y=\"4\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"4.5\" y=\"8\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"8.5\" y=\"8\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"12.5\" y=\"8\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"0.5\" y=\"8\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"0.5\" y=\"12\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"4.5\" y=\"12\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"8.5\" y=\"12\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n        <rect x=\"12.5\" y=\"12\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#FFE0C2\" />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_5055_2832\">\n          <rect\n            width=\"16\"\n            height=\"16\"\n            fill=\"white\"\n            transform=\"translate(0.5)\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/illustrations/command-line.tsx",
    "content": "export function CommandLine() {\n  return (\n    <svg\n      width=\"272\"\n      height=\"271\"\n      viewBox=\"0 0 272 271\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g filter=\"url(#filter0_b_3784_581)\">\n        <path\n          d=\"M78.8887 126.474V145.645L87.1978 140.808L78.8887 126.474ZM78.8887 90.3478V98.3658L86.3244 94.6479L78.8887 90.3478Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M124.936 80.4489L120.322 77.7613L120.21 77.7165L94.8122 63.0467L78.8881 53.864L34.0947 27.9958V174.045L193.201 265.916V119.845L124.936 80.4489ZM177.299 238.458L50.0188 164.974V73.6851L78.8881 90.3482L86.3238 94.6484L94.8122 99.5533L129.751 119.71L152.058 132.588L152.193 132.678L177.299 147.169V238.458Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M177.3 162.824V193.933L163.795 200.674L161.398 201.883L113.648 174.335L116.067 173.126L133.044 164.638L158.442 151.939L177.3 162.824Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M152.193 132.678L122.652 149.521L114.186 153.754L97.2315 162.242L77.8584 171.917L96.0445 161.548L109.684 153.776L112.103 152.567L152.059 132.589L152.193 132.678Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M177.3 212.208V238.458L50.0195 164.974L68.4968 155.746L77.8586 171.917L97.2318 162.241L114.186 153.753L133.044 164.638L116.067 173.126L113.649 174.336V192.589L161.398 220.159L177.3 212.208Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M109.684 153.776L96.0442 161.547L77.8581 171.917L68.4963 155.746L66.727 152.7L78.8884 145.646L87.1975 140.808L78.8884 126.474L66.5254 105.13L77.8581 98.8812L78.8884 100.651L94.8124 128.131L107.399 149.856L109.684 153.776Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M77.8586 98.8812L66.5259 105.13L78.8889 126.474V145.646L66.7275 152.7L68.4968 155.746L50.0195 164.974V73.6849L78.8889 90.3481V98.3661L77.8586 98.8812Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M152.059 132.588L112.103 152.566L109.684 153.775L107.4 149.856L94.8127 128.131L78.8886 100.65L77.8584 98.8809L78.8886 98.3658L86.3243 94.6479L94.8127 99.5528L129.752 119.71L152.059 132.588Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M161.398 201.883V220.159L113.648 192.589V174.335L161.398 201.883Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M177.299 193.932V212.208L161.397 220.159V201.883L163.794 200.674L177.299 193.932Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M237.995 97.4475V243.519L193.201 265.916V119.844L204.108 114.379L237.995 97.4475Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M237.994 97.448L204.108 114.38L193.201 119.845L124.936 80.4489L120.322 77.7613L120.21 77.7165L94.8122 63.0466L78.8881 53.864L34.0947 27.9958L78.8881 5.59912L237.994 97.448Z\"\n          stroke=\"#AFB5AD\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </g>\n      <defs>\n        <filter\n          id=\"filter0_b_3784_581\"\n          x=\"-99.5\"\n          y=\"-100\"\n          width=\"471\"\n          height=\"471\"\n          filterUnits=\"userSpaceOnUse\"\n          colorInterpolationFilters=\"sRGB\"\n        >\n          <feFlood floodOpacity=\"0\" result=\"BackgroundImageFix\" />\n          <feGaussianBlur in=\"BackgroundImageFix\" stdDeviation=\"50\" />\n          <feComposite\n            in2=\"SourceAlpha\"\n            operator=\"in\"\n            result=\"effect1_backgroundBlur_3784_581\"\n          />\n          <feBlend\n            mode=\"normal\"\n            in=\"SourceGraphic\"\n            in2=\"effect1_backgroundBlur_3784_581\"\n            result=\"shape\"\n          />\n        </filter>\n      </defs>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/illustrations/globe.tsx",
    "content": "export function Globe() {\n  return (\n    <svg\n      width=\"270\"\n      height=\"270\"\n      viewBox=\"0 0 324 324\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M229.194 198.798C228.609 191.27 227.388 183.614 225.608 175.832C225.532 175.425 225.43 175.043 225.328 174.662C224.413 170.771 223.344 166.879 222.124 162.937C218.461 151.085 213.832 139.59 208.263 128.501C206.406 124.813 204.448 121.151 202.388 117.565H202.362C202.159 117.158 201.93 116.776 201.701 116.369C194.224 103.475 185.602 91.4445 175.835 80.3303C174.36 78.6771 172.885 77.0497 171.385 75.4219C170.215 74.1757 169.045 72.9549 167.875 71.7596C161.974 65.7574 155.87 60.3656 149.588 55.5841C144.578 51.7692 139.441 48.3357 134.151 45.2837C131.124 43.5543 128.148 41.9772 125.224 40.5784C115.966 36.2039 107.141 33.5339 98.7223 32.6183C98.1119 32.5166 97.5015 32.4655 96.8911 32.4146C86.9977 31.6262 78.2233 32.9233 70.5425 36.3567L66.5749 38.3408C57.9785 43.2494 51.1625 50.9045 46.1522 61.3575C41.1419 71.785 38.6494 84.5778 38.6494 99.736C38.6494 114.894 41.1419 130.586 46.1522 146.813C51.1625 163.064 57.9785 178.578 66.5749 193.406C75.1713 208.233 85.2682 221.866 96.8911 234.328C108.514 246.79 120.925 256.837 134.151 264.466C137.33 266.298 140.509 267.925 143.688 269.299C146.867 270.672 150.072 272.096 153.251 273.572V251.114C150.072 249.638 146.867 248.214 143.688 246.841C140.509 245.442 137.33 243.84 134.151 242.009C130.005 233.031 126.419 224.078 123.392 215.126C120.366 206.148 117.899 197.195 115.991 188.243L143.357 204.062L143.688 204.24V182.317L116.78 166.778L111.693 163.853C111.363 161.157 111.083 158.486 110.905 155.841C110.778 154.646 110.701 153.451 110.625 152.255C110.396 148.466 110.269 144.752 110.269 141.09C110.269 137.428 110.396 133.867 110.625 130.332C110.854 126.822 111.21 123.389 111.693 120.006L152.386 143.506L156.583 145.922C157.066 149.839 157.422 153.705 157.651 157.494C157.905 161.284 158.007 165.023 158.007 168.66C158.007 170.872 157.981 173.06 157.88 175.221C157.829 176.62 157.752 178.019 157.651 179.418C157.473 182.343 157.193 185.217 156.786 188.065C156.735 188.625 156.659 189.21 156.583 189.769L157.524 190.328L164.034 194.067L175.683 200.782C175.937 199.052 176.141 197.323 176.319 195.568C176.497 193.889 176.649 192.185 176.751 190.456C176.827 189.515 176.878 188.574 176.904 187.633C177.056 185.013 177.107 182.368 177.107 179.698C177.107 176.035 177.005 172.322 176.751 168.532C176.7 167.642 176.649 166.778 176.548 165.887C176.344 162.937 176.064 159.961 175.683 156.96L184.381 161.971L194.554 167.846L208.161 175.704C208.695 178.476 209.153 181.198 209.483 183.869C209.661 185.166 209.814 186.437 209.941 187.684C210.348 191.575 210.552 195.339 210.552 199.001C210.552 199.891 210.552 200.807 210.501 201.697C210.45 204.393 210.246 207.013 209.941 209.607C209.56 213.04 208.949 216.347 208.161 219.526L223.217 228.224L227.744 230.843C228.38 227.537 228.838 224.18 229.169 220.696C229.499 217.237 229.652 213.676 229.652 210.014C229.652 206.352 229.499 202.562 229.194 198.798ZM60.1404 134.071C59.3265 129.95 58.7415 125.957 58.3346 122.066C57.9277 118.2 57.7497 114.411 57.7497 110.774C57.7497 107.137 57.9277 103.576 58.3346 100.168C58.7415 96.7348 59.3265 93.4286 60.1404 90.224L92.593 108.968C92.1606 112.02 91.83 115.148 91.6011 118.328L91.5248 119.319C91.2959 122.829 91.1687 126.415 91.1687 130.078C91.1687 133.74 91.2959 137.453 91.5248 141.243C91.7537 145.032 92.1097 148.872 92.593 152.815L60.1404 134.071ZM85.6752 194.703C78.6811 183.996 72.8569 172.653 68.2535 160.674L96.4079 176.925C97.8576 184.53 99.6379 192.185 101.8 199.891C103.936 207.623 106.454 215.431 109.302 223.29C100.554 214.948 92.6693 205.41 85.6752 194.703ZM101.8 72.4971C99.6379 77.7363 97.8576 83.3316 96.4079 89.2575L89.9734 85.5443L68.2535 73.0055C72.8569 66.3421 78.6811 61.7135 85.6752 59.0939C87.3029 58.4835 88.956 58.0003 90.6855 57.6188C93.8646 56.9321 97.1963 56.6526 100.706 56.7798C103.478 56.8561 106.352 57.1864 109.302 57.7968C106.454 62.3493 103.936 67.2579 101.8 72.4971ZM142.391 115.81L115.991 100.575C117.899 93.81 120.366 87.7059 123.392 82.2377C124.868 79.5673 126.47 77.0751 128.199 74.7098C130.03 72.2174 132.014 69.903 134.151 67.7666C136.262 72.37 138.245 76.9736 140.051 81.577C140.839 83.5099 141.577 85.4426 142.315 87.4009C142.493 87.8587 142.645 88.3165 142.823 88.7743C143.535 90.7326 144.222 92.691 144.883 94.6493C147.91 103.602 150.377 112.554 152.284 121.532L142.391 115.81ZM171.868 132.824C170.444 125.245 168.638 117.59 166.501 109.859C165.23 105.357 163.882 100.83 162.356 96.2771C161.313 93.0216 160.169 89.7662 158.973 86.4853C163.221 90.5546 167.29 94.8782 171.13 99.5071C175.174 104.365 179.015 109.553 182.601 115.047C184.635 118.15 186.568 121.329 188.399 124.559C192.901 132.417 196.767 140.607 200.048 149.102L171.868 132.824Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M109.302 223.29C100.553 214.948 92.6687 205.411 85.6746 194.703C78.6805 183.996 72.8563 172.653 68.2529 160.674L96.4076 176.926C97.8573 184.53 99.6373 192.185 101.799 199.891C103.936 207.623 106.453 215.431 109.302 223.29Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M152.285 121.533L142.392 115.81L115.992 100.576L142.315 87.4014C142.493 87.8592 142.646 88.3172 142.824 88.775C143.536 90.7334 144.223 92.6914 144.884 94.6498C147.911 103.602 150.378 112.555 152.285 121.533Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M142.315 87.4009L115.992 100.575C117.9 93.8101 120.367 87.7061 123.393 82.238C124.868 79.5675 126.471 77.0751 128.2 74.7098C130.031 72.2174 132.015 69.903 134.151 67.7666C136.262 72.37 138.246 76.9736 140.052 81.577C140.84 83.5099 141.578 85.4426 142.315 87.4009Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M200.048 149.102L171.868 132.825L188.4 124.56C192.901 132.418 196.767 140.608 200.048 149.102Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M188.399 124.559L171.867 132.824C170.443 125.245 168.637 117.59 166.501 109.858C165.229 105.357 163.881 100.83 162.355 96.2771C161.312 93.0217 160.168 89.7662 158.973 86.4854C163.22 90.5546 167.289 94.8783 171.13 99.5071C175.174 104.365 179.014 109.553 182.6 115.047C184.635 118.15 186.568 121.329 188.399 124.559Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M184.229 214.845L172.352 220.771V222.908L166.095 226.036L134.151 242.008C130.006 233.03 126.42 224.077 123.393 215.125C120.367 206.147 117.9 197.195 115.992 188.242L143.358 204.062L143.689 204.24L158.923 196.635L159.61 196.279L164.035 194.067L175.684 200.781L177.871 199.687C179.804 204.723 181.94 209.784 184.229 214.845Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M209.483 183.868L177.87 199.688L175.683 200.781C175.937 199.052 176.14 197.323 176.318 195.568C176.496 193.889 176.649 192.185 176.751 190.456C176.827 189.515 176.878 188.574 176.903 187.632C177.056 185.013 177.107 182.368 177.107 179.697C177.107 176.035 177.005 172.322 176.751 168.532C176.7 167.642 176.649 166.777 176.547 165.887C176.344 162.937 176.064 159.961 175.683 156.96L184.381 161.971L194.554 167.846L208.161 175.704C208.695 178.476 209.153 181.198 209.483 183.868Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M164.034 194.067L159.609 196.28L158.922 196.636L143.688 204.24V182.317L145.341 181.503L153.505 177.408L157.879 175.221C157.828 176.62 157.752 178.019 157.65 179.417C157.472 182.342 157.192 185.216 156.786 188.065C156.735 188.624 156.658 189.209 156.582 189.768L157.523 190.328L164.034 194.067Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M158.006 168.659C158.006 170.872 157.981 173.059 157.879 175.221L153.504 177.408L145.34 181.503L143.687 182.317L116.779 166.777L111.692 163.852L114.388 162.504L128.402 155.51L142.237 148.592L152.385 143.506L156.582 145.922C157.065 149.839 157.421 153.705 157.65 157.494C157.904 161.284 158.006 165.022 158.006 168.659Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M152.387 143.506L142.239 148.593L128.403 155.51L114.39 162.504L111.694 163.852C111.363 161.157 111.083 158.486 110.905 155.841C110.778 154.645 110.702 153.45 110.626 152.255C110.397 148.465 110.27 144.752 110.27 141.09C110.27 137.427 110.397 133.867 110.626 130.331C110.854 126.822 111.211 123.388 111.694 120.006L152.387 143.506Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M100.706 56.7788L90.1254 62.0689L68.2529 73.0049C72.8563 66.3414 78.6805 61.7126 85.6746 59.093C87.3023 58.4826 88.9557 57.9996 90.6851 57.6181C93.8643 56.9314 97.1958 56.6517 100.706 56.7788Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M109.302 57.7964C106.453 62.3489 103.936 67.2575 101.799 72.4967C99.6373 77.7359 97.8573 83.3312 96.4076 89.2571L89.9728 85.5438L68.2529 73.0053L90.1254 62.0694L100.706 56.7793C103.478 56.8556 106.352 57.186 109.302 57.7964Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M92.5932 152.814L60.1406 134.07L91.6013 118.353L91.525 119.319C91.2961 122.829 91.169 126.415 91.169 130.077C91.169 133.74 91.2961 137.453 91.525 141.242C91.7539 145.032 92.11 148.872 92.5932 152.814Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M92.5924 108.968C92.16 112.02 91.8294 115.148 91.6005 118.327L60.1398 134.07C59.3259 129.95 58.7409 125.957 58.334 122.066C57.9271 118.2 57.749 114.411 57.749 110.774C57.749 107.137 57.9271 103.576 58.334 100.168C58.7409 96.7345 59.3259 93.4282 60.1398 90.2236L92.5924 108.968Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M172.351 222.907V241.575L153.251 251.113C150.071 249.637 146.867 248.213 143.688 246.84C140.509 245.441 137.33 243.839 134.15 242.007L166.094 226.036L172.351 222.907Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M283.848 284.61L270.471 292.494L219.604 317.927L232.982 310.043L269.937 291.579L283.848 284.61Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M277.159 226.494V248.417L266.07 253.962L252.184 260.904L242.316 265.838L240.052 266.982L226.293 273.849V251.927L242.316 243.916L256.228 236.973L256.813 236.667L277.159 226.494Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M232.982 310.04L219.604 317.925L202.411 288.244L191.45 269.322V293.712L172.35 282.7V220.771L174.333 221.916L191.653 231.911L204.116 239.108L223.216 250.146L226.293 251.927V273.849L204.802 261.438L216.298 281.275L232.982 310.04Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M283.85 284.607L269.938 291.576L232.984 310.04L216.299 281.275L204.804 261.438L226.295 273.849L240.054 266.982L242.317 265.837L252.186 260.903L266.072 253.961L283.85 284.607Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M202.414 288.244L191.452 293.712V269.322L202.414 288.244Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M172.35 241.576V264.034L153.25 273.572V251.114L172.35 241.576Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M277.161 226.494L256.815 236.667L256.23 236.972L242.318 243.916L226.295 251.927L223.218 250.147L204.117 239.109L191.655 231.911L174.335 221.916L172.352 220.772L184.229 214.845L210.501 201.697C210.45 204.393 210.247 207.012 209.942 209.606C209.56 213.04 208.95 216.346 208.161 219.525L223.218 228.223L227.745 230.843L258.264 215.583L277.161 226.494Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M280.518 184.581C280.518 188.243 280.366 191.804 280.035 195.263C279.705 198.747 279.247 202.104 278.611 205.411L227.745 230.844C228.381 227.537 228.838 224.18 229.169 220.696C229.5 217.237 229.652 213.676 229.652 210.014C229.652 206.352 229.5 202.562 229.194 198.798C228.61 191.27 227.389 183.615 225.608 175.832C225.532 175.425 225.43 175.044 225.329 174.662C224.413 170.771 223.345 166.88 222.124 162.937C218.462 151.086 213.833 139.59 208.263 128.501C206.406 124.813 204.448 121.151 202.388 117.565H202.363C202.159 117.158 201.93 116.776 201.701 116.369C194.224 103.475 185.602 91.4448 175.836 80.3306C174.361 78.6774 172.886 77.05 171.385 75.4222C170.215 74.176 169.045 72.955 167.875 71.7596C161.975 65.7574 155.871 60.3656 149.589 55.5842C144.579 51.7692 139.441 48.336 134.151 45.2841C131.125 43.5546 128.149 41.9775 125.224 40.5787C115.966 36.2042 107.141 33.534 98.7228 32.6184C98.1124 32.5167 97.502 32.4658 96.8916 32.4149C86.9982 31.6265 78.2238 32.9236 70.543 36.3571L117.442 12.9079C126.038 7.99929 136.135 6.04074 147.758 6.98176C159.381 7.94822 171.792 12.2211 185.017 19.851C198.217 27.4809 210.628 37.527 222.251 49.9892C233.874 62.4514 243.971 76.1089 252.567 90.9364C261.164 105.738 267.98 121.278 272.99 137.504C278.001 153.731 280.518 169.423 280.518 184.581Z\"\n        stroke=\"#EDEDED\"\n        strokeWidth=\"0.755906\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/loading/Loading.styles.tsx",
    "content": "import styled from \"styled-components\";\n\nexport const Container = styled.div`\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  flex: 1;\n`;\n"
  },
  {
    "path": "ui/src/components/loading/Loading.tsx",
    "content": "import * as Styled from \"./Loading.styles\";\n\nexport function Loading() {\n  return <Styled.Container>LOADING...</Styled.Container>;\n}\n"
  },
  {
    "path": "ui/src/components/loading/index.tsx",
    "content": "export { Loading } from \"./Loading\";\n"
  },
  {
    "path": "ui/src/components/sessions/release-session-dialog.tsx",
    "content": "import {\n  DialogHeader,\n  DialogFooter,\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogTitle,\n  DialogDescription,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\n\nimport { useSessionsContext } from \"@/hooks/use-sessions-context\";\nimport { useEffect, useState } from \"react\";\n\nexport const ReleaseSessionDialog = ({\n  children,\n  id,\n}: {\n  id: string;\n  children: React.ReactNode;\n}) => {\n  const [open, setOpen] = useState(false);\n  const { useReleaseSessionMutation } = useSessionsContext();\n\n  const {\n    mutate: releaseSession,\n    isLoading,\n    isSuccess,\n  } = useReleaseSessionMutation();\n\n  useEffect(() => {\n    if (isSuccess) {\n      setOpen(false);\n    }\n  }, [isSuccess]);\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>{children}</DialogTrigger>\n\n      <DialogContent className=\"sm:max-w-[425px] bg-[var(--gray-2)] border-[var(--gray-6)] text-[var(--gray-12)]\">\n        <DialogHeader>\n          <DialogTitle>Are you absolutely sure?</DialogTitle>\n          <DialogDescription>\n            This action cannot be undone. This will release your session and\n            delete all your session data.\n          </DialogDescription>\n        </DialogHeader>\n        <DialogFooter>\n          {!isLoading && (\n            <Button\n              variant=\"outline\"\n              className=\"text-[var(--gray-12)] bg-[var(--gray-3)]\"\n              onClick={() => setOpen(false)}\n            >\n              Cancel\n            </Button>\n          )}\n          <Button\n            type=\"submit\"\n            variant=\"outline\"\n            className=\"text-[var(--red-11)] border-[var(--red-7)] hover:bg-[var(--red-3)]\"\n            disabled={isLoading}\n            onClick={() => {\n              releaseSession(id);\n            }}\n          >\n            {isLoading ? \"Releasing Session...\" : \"Release Session\"}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "ui/src/components/sessions/session-console/index.tsx",
    "content": "import { Tabs, TabsTrigger, TabsList } from \"@/components/ui/tabs\";\nimport { useState } from \"react\";\nimport SessionDetails from \"./session-details\";\nimport SessionLogs from \"./session-logs\";\nimport SessionDevTools from \"./session-devtools\";\n\ninterface SessionConsoleProps {\n  id: string | null;\n}\n\nexport default function SessionConsole({ id }: SessionConsoleProps) {\n  const [activeTab, setActiveTab] = useState<\"details\" | \"logs\" | \"dev-tools\">(\n    \"details\"\n  );\n\n  const tabs: { value: \"details\" | \"logs\" | \"dev-tools\"; label: string }[] = [\n    { value: \"details\", label: \"Details\" },\n    { value: \"logs\", label: \"Logs\" },\n    { value: \"dev-tools\", label: \"Dev Tools\" },\n  ];\n\n  return (\n    <div className=\"flex flex-col w-full h-full\">\n      <div className=\"flex flex-row justify-between items-center bg-[var(--gray-3)] p-2\">\n        <Tabs defaultValue=\"details\">\n          <TabsList className=\"bg-transparent\">\n            {tabs.map((tab) => (\n              <TabsTrigger\n                key={tab.value}\n                value={tab.value}\n                onClick={() => setActiveTab(tab.value)}\n                className={`!bg-transparent !box-shadow-none rounded-none p-4 ${\n                  activeTab === tab.value\n                    ? \"border-b-2 border-b-[var(--gray-11)]\"\n                    : \"\"\n                }`}\n              >\n                {tab.label}\n              </TabsTrigger>\n            ))}\n          </TabsList>\n        </Tabs>\n      </div>\n\n      {activeTab === \"details\" && <SessionDetails id={id} />}\n      {activeTab === \"logs\" && <SessionLogs id={id!} />}\n      {activeTab === \"dev-tools\" && <SessionDevTools />}\n    </div>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/sessions/session-console/session-details.tsx",
    "content": "import { useSessionsContext } from \"@/hooks/use-sessions-context\";\nimport { Skeleton } from \"@radix-ui/themes\";\nimport { ReleaseSessionDialog } from \"../release-session-dialog\";\nimport { Button } from \"@/components/ui/button\";\n\nexport default function SessionDetails({ id }: { id: string | null }) {\n  const { useSession } = useSessionsContext();\n  const { data: session, isLoading, isError } = useSession(id!);\n\n  return (\n    <div className=\"w-full h-full overflow-y-auto overflow-x-scroll bg-[var(--gray-2)] p-3 pt-8 font-mono text-xs flex flex-col\">\n      {isLoading && (\n        <>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n          <div className=\"flex flex-col gap-2 py-2 border-b border-[var(--gray-6)]\">\n            <Skeleton className=\"w-full h-4 border-b border-[var(--gray-6)]\" />\n          </div>\n        </>\n      )}\n      {isError && <div>Error loading session</div>}\n      {session && (\n        <>\n          <div className=\"flex w-full flex-row gap-2 justify-between py-2 border-b border-[var(--gray-6)]\">\n            <div className=\"text-[var(--gray-11)]\">ID</div>\n            <div className=\"text-right\">{session.id}</div>\n          </div>\n\n          <div className=\"flex w-full flex-row gap-2 justify-between py-2 border-b border-[var(--gray-6)]\">\n            <div className=\"text-[var(--gray-11)]\">Timestamp</div>\n            <div className=\"text-right\">\n              {session.createdAt.toLocaleString()}\n            </div>\n          </div>\n\n          <div className=\"flex w-full flex-row gap-2 justify-between py-2 border-b border-[var(--gray-6)]\">\n            <div className=\"text-[var(--gray-11)]\">Duration</div>\n            <div className=\"text-right\">{session.duration}</div>\n          </div>\n\n          <div className=\"flex w-full flex-row gap-2 justify-between py-2 border-b border-[var(--gray-6)]\">\n            <div className=\"text-[var(--gray-11)]\">User Agent</div>\n            <div className=\"text-right\">{session.userAgent}</div>\n          </div>\n\n          <div className=\"flex w-full flex-row gap-2 justify-between py-2 border-b border-[var(--gray-6)]\">\n            <div className=\"text-[var(--gray-11)]\">Auto-captcha</div>\n            <div className=\"text-right\">{session.solveCaptcha?.toString()}</div>\n          </div>\n\n          <div className=\"flex w-full flex-row gap-2 justify-between py-2 border-b border-[var(--gray-6)]\">\n            <div className=\"text-[var(--gray-11)]\">isSelenium</div>\n            <div className=\"text-right\">{session.isSelenium?.toString()}</div>\n          </div>\n\n          <div className=\"flex w-full flex-row gap-2 justify-between py-2 border-b border-[var(--gray-6)]\">\n            <div className=\"text-[var(--gray-11)]\">Websocket URL</div>\n            <div className=\"text-right\">\n              {session.websocketUrl.slice(0, 30)}\n            </div>\n          </div>\n        </>\n      )}\n      {session?.status === \"live\" && (\n        <div className=\"mt-auto border-t border-[var(--gray-6)] py-4\">\n          <ReleaseSessionDialog id={id!}>\n            <Button\n              variant=\"outline\"\n              className=\"flex w-full bg-transparent text-[var(--red-11)] border-[var(--red-7)] hover:bg-[var(--red-3)]\"\n            >\n              Release Session\n            </Button>\n          </ReleaseSessionDialog>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/sessions/session-console/session-devtools.tsx",
    "content": "import { env } from \"@/env\";\nimport { useEffect, useState } from \"react\";\n\nexport default function SessionDevTools() {\n  const [pageId, setPageId] = useState<string | null>(null);\n\n  useEffect(() => {\n    const ws = new WebSocket(`${env.VITE_API_URL}/v1/sessions/pageId`);\n\n    ws.onmessage = (event) => {\n      setPageId(event.data.pageId);\n    };\n\n    return () => {\n      ws.close();\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!pageId) return;\n\n    const iframe = document.querySelector(\"iframe\");\n    if (iframe) {\n      iframe.src = iframe.src + \"\";\n    }\n  }, [pageId]);\n\n  return (\n    <iframe\n      src={`${env.VITE_API_URL}/v1/devtools/inspector.html${\n        pageId ? `?pageId=${pageId}` : \"\"\n      }`}\n      className=\"w-full h-full\"\n    />\n  );\n}\n"
  },
  {
    "path": "ui/src/components/sessions/session-console/session-logs.tsx",
    "content": "import { env } from \"@/env\";\nimport { useEffect, useState, useRef } from \"react\";\n\nexport default function SessionLogs({ id }: { id: string }) {\n  const [logs, setLogs] = useState<any[]>([]);\n  const consoleRef = useRef<HTMLDivElement>(null);\n  useEffect(() => {\n    const connectWebSocket = async () => {\n      const ws = new WebSocket(`${env.VITE_WS_URL}/v1/sessions/logs`);\n\n      ws.onmessage = (event) => {\n        const logs = JSON.parse(event.data);\n        setLogs((prevLogs) =>\n          [...prevLogs, ...logs].sort(\n            (a, b) =>\n              new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()\n          )\n        );\n      };\n\n      return () => {\n        ws.close();\n      };\n    };\n    connectWebSocket();\n  }, [id]);\n\n  useEffect(() => {\n    if (consoleRef.current) {\n      consoleRef.current.scrollTop = consoleRef.current.scrollHeight;\n    }\n  }, [logs]);\n  const logTypeToColor = (type: string) => {\n    if (type === \"Console\") return \"var(--cyan-a11)\";\n    if (type === \"Request\") return \"var(--pink-a11)\";\n    if (type === \"Response\") return \"var(--green-a11)\";\n    if (type === \"Error\") return \"var(--red-a11)\";\n    return \"var(--gray-11)\";\n  };\n\n  const logTypeToFormat = (type: string, log: Record<string, any>) => {\n    if (type === \"Console\") {\n      if (log.message) {\n        return log.message\n          .replace(/^\\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\s+(INFO|WARN|ERROR|DEBUG)\\s+/, \"\")\n          .replace(\"\\n\", \"\")\n          .replace(\"\\t\", \"\");\n      }\n      return log.text;\n    }\n    if (type === \"Request\") return `[${log.method}] ${log.url}`;\n    if (type === \"Response\") return `[${log.status}] ${log.url}`;\n    if (type === \"Error\") return log.message;\n    if (type === \"Navigation\") return log.url || JSON.stringify(log);\n    return log.message || JSON.stringify(log);\n  };\n\n  return (\n    <div\n      ref={consoleRef}\n      className=\"w-full h-full overflow-y-auto overflow-x-scroll bg-[var(--gray-2)] p-2 font-mono text-xs flex flex-col\"\n    >\n      {logs.length === 0 && <p className=\"text-gray-400\">No new logs...</p>}\n      {logs &&\n        logs.slice(-40).map((log) => {\n          const logBody = JSON.parse(log.text);\n          const cleanMessage = logTypeToFormat(log.type, logBody);\n\n          return (\n            <pre key={log.id} className=\"text-overflow-ellipsis mb-1\">\n              <span className=\"text-gray-400\">\n                {new Date(log.timestamp).toLocaleTimeString(\"en-US\", {\n                  hour: \"2-digit\",\n                  minute: \"2-digit\",\n                  second: \"2-digit\",\n                  hour12: false,\n                })}\n              </span>{\" \"}\n              <span style={{ color: logTypeToColor(log.type) }}>\n                [{log.type}]\n              </span>{\" \"}\n              {cleanMessage}\n            </pre>\n          );\n        })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/sessions/session-viewer/empty-state.tsx",
    "content": "import { Globe } from \"@/components/illustrations/globe\";\nimport { ArrowTopRightIcon } from \"@radix-ui/react-icons\";\nimport { Link } from \"react-router-dom\";\n\nexport function EmptyState() {\n  return (\n    <div className=\"flex flex-col w-full flex-1 px-24 pt-10 bg-[url('/grid.svg')] bg-no-repeat bg-center border-[var(--gray-6)] bg-cover items-center\">\n      <div className=\"flex flex-col gap-4 max-w-[396px] mx-auto\">\n        <div className=\"flex flex-col gap-4 mx-auto\">\n          <h1 className=\"text-primary text-2xl  font-medium text-center\">\n            This session has no events!\n          </h1>\n          <p className=\"text-muted-foreground text-lg text-center max-w-[280px]\">\n            Double check the logs if you're expecting events.\n          </p>\n        </div>\n        <div className=\"flex justify-center items-center w-full\">\n          <Globe />\n        </div>\n        <div className=\"flex flex-col gap-2 justify-center items-center\">\n          <p className=\"text-muted-foreground text-lg text-center max-w-[280px]\">\n            If you think this is an error, message us on Discord.\n          </p>\n          <Link\n            to=\"https://discord.gg/steel-dev\"\n            target=\"_blank\"\n            className=\"flex items-center py-2 gap-1 text-[var(--indigo-11)] text-sm hover:text-[var(--indigo-12)] cursor-pointer\"\n          >\n            Go to Discord\n            <ArrowTopRightIcon width={16} height={16} />\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/sessions/session-viewer/example-events/example-events.json",
    "content": "[\n    {\n        \"type\": 4,\n        \"data\": {\n            \"href\": \"https://fw8kb.csb.app/\",\n            \"width\": 632,\n            \"height\": 397\n        },\n        \"timestamp\": 1617045231476\n    },\n    {\n        \"type\": 2,\n        \"data\": {\n            \"node\": {\n                \"type\": 0,\n                \"childNodes\": [\n                    {\n                        \"type\": 2,\n                        \"tagName\": \"html\",\n                        \"attributes\": {},\n                        \"childNodes\": [\n                            {\n                                \"type\": 2,\n                                \"tagName\": \"head\",\n                                \"attributes\": {},\n                                \"childNodes\": [\n                                    {\n                                        \"type\": 3,\n                                        \"textContent\": \"\\n    \",\n                                        \"id\": 4\n                                    },\n                                    {\n                                        \"type\": 2,\n                                        \"tagName\": \"script\",\n                                        \"attributes\": {\n                                            \"crossorigin\": \"\",\n                                            \"type\": \"text/javascript\",\n                                            \"src\": \"https://codesandbox.io/static/js/vendors~app~embed~sandbox-startup.6e3433fd3.chunk.js\"\n                                        },\n                                        \"childNodes\": [],\n                                        \"id\": 5\n                                    },\n                                    {\n                                        \"type\": 3,\n                                        \"textContent\": \"\\n    \",\n                                        \"id\": 6\n                                    },\n                                    {\n                                        \"type\": 2,\n                                        \"tagName\": \"script\",\n                                        \"attributes\": {\n                                            \"crossorigin\": \"\",\n                                            \"type\": \"text/javascript\",\n                                            \"src\": \"https://codesandbox.io/static/js/sandbox-startup.8867d3bba.js\"\n                                        },\n                                        \"childNodes\": [],\n                                        \"id\": 7\n                                    },\n                                    {\n                                        \"type\": 2,\n                                        \"tagName\": \"link\",\n                                        \"attributes\": {\n                                            \"href\": \"https://codesandbox.io/static/js/babel.7.12.12.min.js\",\n                                            \"rel\": \"preload\",\n                                            \"as\": \"script\"\n                                        },\n                                        \"childNodes\": [],\n                                        \"id\": 8\n                                    },\n                                    {\n                                        \"type\": 3,\n                                        \"textContent\": \"\\n\\n\",\n                                        \"id\": 9\n                                    },\n                                    {\n                                        \"type\": 2,\n                                        \"tagName\": \"script\",\n                                        \"attributes\": {\n                                            \"src\": \"https://codesandbox.io/static/browserfs8/browserfs.min.js\",\n                                            \"type\": \"text/javascript\"\n                                        },\n                                        \"childNodes\": [],\n                                        \"id\": 10\n                                    },\n                                    {\n                                        \"type\": 3,\n                                        \"textContent\": \"\\n\\n\",\n                                        \"id\": 11\n                                    },\n                                    {\n                                        \"type\": 2,\n                                        \"tagName\": \"script\",\n                                        \"attributes\": {},\n                                        \"childNodes\": [\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"SCRIPT_PLACEHOLDER\",\n                                                \"id\": 13\n                                            }\n                                        ],\n                                        \"id\": 12\n                                    },\n                                    {\n                                        \"type\": 3,\n                                        \"textContent\": \"\\n  \",\n                                        \"id\": 14\n                                    },\n                                    {\n                                        \"type\": 2,\n                                        \"tagName\": \"link\",\n                                        \"attributes\": {\n                                            \"rel\": \"manifest\",\n                                            \"href\": \"https://fw8kb.csb.app/manifest.json\"\n                                        },\n                                        \"childNodes\": [],\n                                        \"id\": 15\n                                    },\n                                    {\n                                        \"type\": 3,\n                                        \"textContent\": \"\\n  \",\n                                        \"id\": 16\n                                    },\n                                    {\n                                        \"type\": 2,\n                                        \"tagName\": \"script\",\n                                        \"attributes\": {\n                                            \"charset\": \"utf-8\",\n                                            \"src\": \"https://codesandbox.io/static/js/vendors~react-devtools-backend.d7ac7a663.chunk.js\"\n                                        },\n                                        \"childNodes\": [],\n                                        \"id\": 17\n                                    }\n                                ],\n                                \"id\": 3\n                            },\n                            {\n                                \"type\": 3,\n                                \"textContent\": \"\\n\",\n                                \"id\": 18\n                            },\n                            {\n                                \"type\": 2,\n                                \"tagName\": \"body\",\n                                \"attributes\": {},\n                                \"childNodes\": [\n                                    {\n                                        \"type\": 3,\n                                        \"textContent\": \"\\n\",\n                                        \"id\": 20\n                                    },\n                                    {\n                                        \"type\": 2,\n                                        \"tagName\": \"div\",\n                                        \"attributes\": {\n                                            \"id\": \"root\"\n                                        },\n                                        \"childNodes\": [\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n  \",\n                                                \"id\": 22\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {\n                                                    \"crossorigin\": \"\",\n                                                    \"type\": \"text/javascript\",\n                                                    \"src\": \"https://codesandbox.io/static/js/vendors~app~codemirror-editor~monaco-editor~sandbox.5ca13c344.chunk.js\"\n                                                },\n                                                \"childNodes\": [],\n                                                \"id\": 23\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n    \",\n                                                \"id\": 24\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {\n                                                    \"crossorigin\": \"\",\n                                                    \"type\": \"text/javascript\",\n                                                    \"src\": \"https://codesandbox.io/static/js/common-sandbox.51de09afd.chunk.js\"\n                                                },\n                                                \"childNodes\": [],\n                                                \"id\": 25\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n    \",\n                                                \"id\": 26\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {\n                                                    \"crossorigin\": \"\",\n                                                    \"type\": \"text/javascript\",\n                                                    \"src\": \"https://codesandbox.io/static/js/vendors~app~sandbox.5844197cd.chunk.js\"\n                                                },\n                                                \"childNodes\": [],\n                                                \"id\": 27\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n    \",\n                                                \"id\": 28\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {\n                                                    \"crossorigin\": \"\",\n                                                    \"type\": \"text/javascript\",\n                                                    \"src\": \"https://codesandbox.io/static/js/vendors~sandbox.ef7aff03a.chunk.js\"\n                                                },\n                                                \"childNodes\": [],\n                                                \"id\": 29\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n    \",\n                                                \"id\": 30\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {\n                                                    \"crossorigin\": \"\",\n                                                    \"type\": \"text/javascript\",\n                                                    \"src\": \"https://codesandbox.io/static/js/default~app~embed~sandbox.4dff2e362.chunk.js\"\n                                                },\n                                                \"childNodes\": [],\n                                                \"id\": 31\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n    \",\n                                                \"id\": 32\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {\n                                                    \"crossorigin\": \"\",\n                                                    \"type\": \"text/javascript\",\n                                                    \"src\": \"https://codesandbox.io/static/js/sandbox.3fb0c3bb6.js\"\n                                                },\n                                                \"childNodes\": [],\n                                                \"id\": 33\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n    \",\n                                                \"id\": 34\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {\n                                                    \"crossorigin\": \"\",\n                                                    \"type\": \"text/javascript\",\n                                                    \"src\": \"https://codesandbox.io/static/js/banner.8d93e521a.js\"\n                                                },\n                                                \"childNodes\": [],\n                                                \"id\": 35\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n  \",\n                                                \"id\": 36\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {},\n                                                \"childNodes\": [\n                                                    {\n                                                        \"type\": 3,\n                                                        \"textContent\": \"SCRIPT_PLACEHOLDER\",\n                                                        \"id\": 38\n                                                    }\n                                                ],\n                                                \"id\": 37\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n  \",\n                                                \"id\": 39\n                                            },\n                                            {\n                                                \"type\": 2,\n                                                \"tagName\": \"script\",\n                                                \"attributes\": {\n                                                    \"crossorigin\": \"\",\n                                                    \"type\": \"text/javascript\",\n                                                    \"src\": \"https://codesandbox.io/static/js/watermark-button.be960f43b.js\"\n                                                },\n                                                \"childNodes\": [],\n                                                \"id\": 40\n                                            },\n                                            {\n                                                \"type\": 3,\n                                                \"textContent\": \"\\n  \\n\",\n                                                \"id\": 41\n                                            }\n                                        ],\n                                        \"id\": 21\n                                    }\n                                ],\n                                \"id\": 19\n                            }\n                        ],\n                        \"id\": 2\n                    }\n                ],\n                \"id\": 1\n            },\n            \"initialOffset\": {\n                \"left\": 0,\n                \"top\": 0\n            }\n        },\n        \"timestamp\": 1617045231479\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [\n                {\n                    \"parentId\": 21,\n                    \"id\": 41\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 40\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 39\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 37\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 36\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 35\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 34\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 33\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 32\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 31\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 30\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 29\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 28\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 27\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 26\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 25\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 24\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 23\n                },\n                {\n                    \"parentId\": 21,\n                    \"id\": 22\n                }\n            ],\n            \"adds\": [\n                {\n                    \"parentId\": 3,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"style\",\n                        \"attributes\": {\n                            \"media\": \"\",\n                            \"_cssText\": \".ae { margin-left: 20px; }.af { display: inline-flex; }.ag { -webkit-box-orient: horizontal; -webkit-box-direction: normal; flex-direction: row; }.ah { -webkit-box-align: center; align-items: center; }.ai { -webkit-box-pack: center; justify-content: center; }.aj { border-left-width: 0px; }.ak { border-top-width: 0px; }.al { border-right-width: 0px; }.am { border-bottom-width: 0px; }.an { border-left-style: none; }.ao { border-top-style: none; }.ap { border-right-style: none; }.aq { border-bottom-style: none; }.ar { outline: none; }.as { box-shadow: none; }.at { text-decoration: none; }.au { appearance: none; }.av { transition-property: background; }.aw { transition-duration: 200ms; }.ax { transition-timing-function: cubic-bezier(0, 0, 1, 1); }.ay { cursor: pointer; }.az:disabled { cursor: not-allowed; }.b0:disabled { background-color: rgb(246, 246, 246); }.b1:disabled { color: rgb(175, 175, 175); }.b2 { margin-left: 0px; }.b3 { margin-top: 0px; }.b4 { margin-right: 0px; }.b5 { margin-bottom: 0px; }.b6 { font-family: system-ui, \\\"Helvetica Neue\\\", Helvetica, Arial, sans-serif; }.b7 { font-size: 16px; }.b8 { font-weight: 500; }.b9 { line-height: 20px; }.ba { border-top-right-radius: 0px; }.bb { border-bottom-right-radius: 0px; }.bc { border-top-left-radius: 0px; }.bd { border-bottom-left-radius: 0px; }.be { padding-top: 14px; }.bf { padding-bottom: 14px; }.bg { padding-left: 16px; }.bh { padding-right: 16px; }.bi { color: rgb(255, 255, 255); }.bj { background-color: rgb(0, 0, 0); }.bk:hover { background-color: rgb(51, 51, 51); }.bl:active { background-color: rgb(84, 84, 84); }\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 42\n                    }\n                },\n                {\n                    \"parentId\": 21,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"div\",\n                        \"attributes\": {\n                            \"class\": \"\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 43\n                    }\n                },\n                {\n                    \"parentId\": 21,\n                    \"nextId\": 43,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"div\",\n                        \"attributes\": {\n                            \"class\": \"\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 44\n                    }\n                },\n                {\n                    \"parentId\": 44,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"div\",\n                        \"attributes\": {\n                            \"class\": \"ae\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 45\n                    }\n                },\n                {\n                    \"parentId\": 44,\n                    \"nextId\": 45,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"button\",\n                        \"attributes\": {\n                            \"data-baseweb\": \"button\",\n                            \"class\": \"af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 46\n                    }\n                },\n                {\n                    \"parentId\": 46,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 3,\n                        \"textContent\": \"Hello\",\n                        \"id\": 47\n                    }\n                },\n                {\n                    \"parentId\": 45,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"button\",\n                        \"attributes\": {\n                            \"data-baseweb\": \"button\",\n                            \"class\": \"af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 48\n                    }\n                },\n                {\n                    \"parentId\": 48,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 3,\n                        \"textContent\": \"set\",\n                        \"id\": 49\n                    }\n                }\n            ]\n        },\n        \"timestamp\": 1617045231507\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 106,\n                    \"y\": 167,\n                    \"id\": 19,\n                    \"timeOffset\": 0\n                }\n            ]\n        },\n        \"timestamp\": 1617045231550\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [],\n            \"adds\": [\n                {\n                    \"parentId\": 3,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"script\",\n                        \"attributes\": {\n                            \"charset\": \"utf-8\",\n                            \"src\": \"https://codesandbox.io/static/js/7.4b9a6a838.chunk.js\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 50\n                    }\n                },\n                {\n                    \"parentId\": 3,\n                    \"nextId\": 50,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"script\",\n                        \"attributes\": {\n                            \"charset\": \"utf-8\",\n                            \"src\": \"https://codesandbox.io/static/js/2.9e4369f2f.chunk.js\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 51\n                    }\n                },\n                {\n                    \"parentId\": 3,\n                    \"nextId\": 51,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"script\",\n                        \"attributes\": {\n                            \"charset\": \"utf-8\",\n                            \"src\": \"https://codesandbox.io/static/js/0.84205e20b.chunk.js\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 52\n                    }\n                }\n            ]\n        },\n        \"timestamp\": 1617045232111\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 107,\n                    \"y\": 164,\n                    \"id\": 19,\n                    \"timeOffset\": 0\n                }\n            ]\n        },\n        \"timestamp\": 1617045232706\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 106,\n                    \"y\": 163,\n                    \"id\": 19,\n                    \"timeOffset\": -446\n                },\n                {\n                    \"x\": 105,\n                    \"y\": 162,\n                    \"id\": 19,\n                    \"timeOffset\": -394\n                },\n                {\n                    \"x\": 105,\n                    \"y\": 161,\n                    \"id\": 19,\n                    \"timeOffset\": -72\n                },\n                {\n                    \"x\": 102,\n                    \"y\": 159,\n                    \"id\": 19,\n                    \"timeOffset\": -14\n                }\n            ]\n        },\n        \"timestamp\": 1617045233206\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 97,\n                    \"y\": 152,\n                    \"id\": 19,\n                    \"timeOffset\": -464\n                },\n                {\n                    \"x\": 87,\n                    \"y\": 139,\n                    \"id\": 19,\n                    \"timeOffset\": -397\n                },\n                {\n                    \"x\": 79,\n                    \"y\": 124,\n                    \"id\": 19,\n                    \"timeOffset\": -331\n                },\n                {\n                    \"x\": 73,\n                    \"y\": 113,\n                    \"id\": 19,\n                    \"timeOffset\": -281\n                },\n                {\n                    \"x\": 68,\n                    \"y\": 101,\n                    \"id\": 48,\n                    \"timeOffset\": -229\n                },\n                {\n                    \"x\": 65,\n                    \"y\": 95,\n                    \"id\": 48,\n                    \"timeOffset\": -164\n                },\n                {\n                    \"x\": 64,\n                    \"y\": 92,\n                    \"id\": 48,\n                    \"timeOffset\": -113\n                },\n                {\n                    \"x\": 63,\n                    \"y\": 91,\n                    \"id\": 48,\n                    \"timeOffset\": -47\n                }\n            ]\n        },\n        \"timestamp\": 1617045233707\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 63,\n                    \"y\": 91,\n                    \"id\": 48,\n                    \"timeOffset\": 0\n                }\n            ]\n        },\n        \"timestamp\": 1617045234889\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 64,\n                    \"y\": 88,\n                    \"id\": 48,\n                    \"timeOffset\": -449\n                },\n                {\n                    \"x\": 64,\n                    \"y\": 77,\n                    \"id\": 48,\n                    \"timeOffset\": -383\n                },\n                {\n                    \"x\": 64,\n                    \"y\": 58,\n                    \"id\": 48,\n                    \"timeOffset\": -333\n                },\n                {\n                    \"x\": 60,\n                    \"y\": 44,\n                    \"id\": 46,\n                    \"timeOffset\": -283\n                },\n                {\n                    \"x\": 59,\n                    \"y\": 36,\n                    \"id\": 46,\n                    \"timeOffset\": -233\n                },\n                {\n                    \"x\": 57,\n                    \"y\": 33,\n                    \"id\": 46,\n                    \"timeOffset\": -183\n                }\n            ]\n        },\n        \"timestamp\": 1617045235392\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 57,\n                    \"y\": 34,\n                    \"id\": 46,\n                    \"timeOffset\": -236\n                },\n                {\n                    \"x\": 59,\n                    \"y\": 54,\n                    \"id\": 46,\n                    \"timeOffset\": -183\n                },\n                {\n                    \"x\": 60,\n                    \"y\": 66,\n                    \"id\": 48,\n                    \"timeOffset\": -116\n                },\n                {\n                    \"x\": 60,\n                    \"y\": 70,\n                    \"id\": 48,\n                    \"timeOffset\": -50\n                }\n            ]\n        },\n        \"timestamp\": 1617045235893\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 48,\n            \"x\": 60,\n            \"y\": 74\n        },\n        \"timestamp\": 1617045236173\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 5,\n            \"id\": 48\n        },\n        \"timestamp\": 1617045236174\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 48,\n            \"x\": 60,\n            \"y\": 74\n        },\n        \"timestamp\": 1617045236308\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 48,\n            \"x\": 60,\n            \"y\": 74\n        },\n        \"timestamp\": 1617045236310\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 8,\n            \"id\": 42,\n            \"adds\": [\n                {\n                    \"rule\": \".bm{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}\",\n                    \"index\": 44\n                }\n            ]\n        },\n        \"timestamp\": 1617045236312\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 8,\n            \"id\": 42,\n            \"adds\": [\n                {\n                    \"rule\": \".bn{color:red}\",\n                    \"index\": 45\n                }\n            ]\n        },\n        \"timestamp\": 1617045236312\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [],\n            \"adds\": [\n                {\n                    \"parentId\": 44,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"div\",\n                        \"attributes\": {\n                            \"class\": \"bm bn\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 53\n                    }\n                },\n                {\n                    \"parentId\": 53,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 3,\n                        \"textContent\": \"test qweqwe\",\n                        \"id\": 54\n                    }\n                }\n            ]\n        },\n        \"timestamp\": 1617045236316\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 60,\n                    \"y\": 73,\n                    \"id\": 48,\n                    \"timeOffset\": -485\n                },\n                {\n                    \"x\": 60,\n                    \"y\": 74,\n                    \"id\": 48,\n                    \"timeOffset\": -405\n                },\n                {\n                    \"x\": 60,\n                    \"y\": 74,\n                    \"id\": 48,\n                    \"timeOffset\": -324\n                }\n            ]\n        },\n        \"timestamp\": 1617045236394\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 60,\n                    \"y\": 74,\n                    \"id\": 48,\n                    \"timeOffset\": -64\n                },\n                {\n                    \"x\": 68,\n                    \"y\": 83,\n                    \"id\": 48,\n                    \"timeOffset\": -1\n                }\n            ]\n        },\n        \"timestamp\": 1617045236894\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 96,\n                    \"y\": 108,\n                    \"id\": 53,\n                    \"timeOffset\": -450\n                },\n                {\n                    \"x\": 116,\n                    \"y\": 121,\n                    \"id\": 53,\n                    \"timeOffset\": -400\n                },\n                {\n                    \"x\": 123,\n                    \"y\": 126,\n                    \"id\": 19,\n                    \"timeOffset\": -335\n                },\n                {\n                    \"x\": 124,\n                    \"y\": 128,\n                    \"id\": 19,\n                    \"timeOffset\": -285\n                },\n                {\n                    \"x\": 124,\n                    \"y\": 128,\n                    \"id\": 19,\n                    \"timeOffset\": -235\n                },\n                {\n                    \"x\": 123,\n                    \"y\": 128,\n                    \"id\": 19,\n                    \"timeOffset\": -135\n                },\n                {\n                    \"x\": 123,\n                    \"y\": 128,\n                    \"id\": 19,\n                    \"timeOffset\": -67\n                },\n                {\n                    \"x\": 122,\n                    \"y\": 128,\n                    \"id\": 19,\n                    \"timeOffset\": -7\n                }\n            ]\n        },\n        \"timestamp\": 1617045237394\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 122,\n                    \"y\": 127,\n                    \"id\": 19,\n                    \"timeOffset\": -452\n                },\n                {\n                    \"x\": 121,\n                    \"y\": 127,\n                    \"id\": 19,\n                    \"timeOffset\": -401\n                },\n                {\n                    \"x\": 120,\n                    \"y\": 127,\n                    \"id\": 19,\n                    \"timeOffset\": -351\n                },\n                {\n                    \"x\": 117,\n                    \"y\": 126,\n                    \"id\": 19,\n                    \"timeOffset\": -285\n                },\n                {\n                    \"x\": 112,\n                    \"y\": 125,\n                    \"id\": 19,\n                    \"timeOffset\": -235\n                },\n                {\n                    \"x\": 109,\n                    \"y\": 123,\n                    \"id\": 19,\n                    \"timeOffset\": -185\n                },\n                {\n                    \"x\": 105,\n                    \"y\": 121,\n                    \"id\": 53,\n                    \"timeOffset\": -134\n                },\n                {\n                    \"x\": 101,\n                    \"y\": 120,\n                    \"id\": 53,\n                    \"timeOffset\": -84\n                },\n                {\n                    \"x\": 98,\n                    \"y\": 118,\n                    \"id\": 53,\n                    \"timeOffset\": -18\n                }\n            ]\n        },\n        \"timestamp\": 1617045237894\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 97,\n                    \"y\": 117,\n                    \"id\": 53,\n                    \"timeOffset\": -467\n                },\n                {\n                    \"x\": 95,\n                    \"y\": 117,\n                    \"id\": 53,\n                    \"timeOffset\": -402\n                },\n                {\n                    \"x\": 93,\n                    \"y\": 116,\n                    \"id\": 53,\n                    \"timeOffset\": -352\n                },\n                {\n                    \"x\": 91,\n                    \"y\": 116,\n                    \"id\": 53,\n                    \"timeOffset\": -302\n                },\n                {\n                    \"x\": 90,\n                    \"y\": 116,\n                    \"id\": 53,\n                    \"timeOffset\": -251\n                },\n                {\n                    \"x\": 89,\n                    \"y\": 116,\n                    \"id\": 53,\n                    \"timeOffset\": -201\n                },\n                {\n                    \"x\": 88,\n                    \"y\": 116,\n                    \"id\": 53,\n                    \"timeOffset\": -151\n                },\n                {\n                    \"x\": 87,\n                    \"y\": 116,\n                    \"id\": 53,\n                    \"timeOffset\": -85\n                },\n                {\n                    \"x\": 85,\n                    \"y\": 116,\n                    \"id\": 53,\n                    \"timeOffset\": -21\n                }\n            ]\n        },\n        \"timestamp\": 1617045238394\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 53,\n            \"x\": 85,\n            \"y\": 116\n        },\n        \"timestamp\": 1617045238412\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 6,\n            \"id\": 48\n        },\n        \"timestamp\": 1617045238412\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 53,\n            \"x\": 85,\n            \"y\": 116\n        },\n        \"timestamp\": 1617045238491\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 53,\n            \"x\": 85,\n            \"y\": 116\n        },\n        \"timestamp\": 1617045238492\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 53,\n            \"x\": 85,\n            \"y\": 116\n        },\n        \"timestamp\": 1617045238581\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 53,\n            \"x\": 85,\n            \"y\": 116\n        },\n        \"timestamp\": 1617045238701\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 53,\n            \"x\": 85,\n            \"y\": 116\n        },\n        \"timestamp\": 1617045238701\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 4,\n            \"id\": 53,\n            \"x\": 85,\n            \"y\": 116\n        },\n        \"timestamp\": 1617045238701\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 85,\n                    \"y\": 116,\n                    \"id\": 53,\n                    \"timeOffset\": 0\n                }\n            ]\n        },\n        \"timestamp\": 1617045239191\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 19,\n            \"x\": 82,\n            \"y\": 123\n        },\n        \"timestamp\": 1617045239551\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 19,\n            \"x\": 82,\n            \"y\": 123\n        },\n        \"timestamp\": 1617045239662\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 19,\n            \"x\": 82,\n            \"y\": 123\n        },\n        \"timestamp\": 1617045239662\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 85,\n                    \"y\": 117,\n                    \"id\": 53,\n                    \"timeOffset\": -449\n                },\n                {\n                    \"x\": 85,\n                    \"y\": 119,\n                    \"id\": 53,\n                    \"timeOffset\": -383\n                },\n                {\n                    \"x\": 84,\n                    \"y\": 121,\n                    \"id\": 53,\n                    \"timeOffset\": -332\n                },\n                {\n                    \"x\": 83,\n                    \"y\": 122,\n                    \"id\": 19,\n                    \"timeOffset\": -266\n                },\n                {\n                    \"x\": 82,\n                    \"y\": 123,\n                    \"id\": 19,\n                    \"timeOffset\": -209\n                }\n            ]\n        },\n        \"timestamp\": 1617045239692\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 82,\n                    \"y\": 123,\n                    \"id\": 19,\n                    \"timeOffset\": 0\n                }\n            ]\n        },\n        \"timestamp\": 1617045242180\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 117,\n                    \"y\": 108,\n                    \"id\": 53,\n                    \"timeOffset\": -437\n                },\n                {\n                    \"x\": 149,\n                    \"y\": 96,\n                    \"id\": 45,\n                    \"timeOffset\": -372\n                },\n                {\n                    \"x\": 155,\n                    \"y\": 91,\n                    \"id\": 45,\n                    \"timeOffset\": -321\n                },\n                {\n                    \"x\": 158,\n                    \"y\": 86,\n                    \"id\": 45,\n                    \"timeOffset\": -271\n                },\n                {\n                    \"x\": 158,\n                    \"y\": 84,\n                    \"id\": 45,\n                    \"timeOffset\": -221\n                },\n                {\n                    \"x\": 157,\n                    \"y\": 82,\n                    \"id\": 45,\n                    \"timeOffset\": -171\n                },\n                {\n                    \"x\": 146,\n                    \"y\": 78,\n                    \"id\": 45,\n                    \"timeOffset\": -105\n                },\n                {\n                    \"x\": 137,\n                    \"y\": 76,\n                    \"id\": 45,\n                    \"timeOffset\": -55\n                },\n                {\n                    \"x\": 131,\n                    \"y\": 75,\n                    \"id\": 45,\n                    \"timeOffset\": -5\n                }\n            ]\n        },\n        \"timestamp\": 1617045242681\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 127,\n                    \"y\": 74,\n                    \"id\": 45,\n                    \"timeOffset\": -454\n                },\n                {\n                    \"x\": 124,\n                    \"y\": 74,\n                    \"id\": 45,\n                    \"timeOffset\": -386\n                },\n                {\n                    \"x\": 124,\n                    \"y\": 74,\n                    \"id\": 45,\n                    \"timeOffset\": -320\n                },\n                {\n                    \"x\": 124,\n                    \"y\": 74,\n                    \"id\": 45,\n                    \"timeOffset\": -224\n                }\n            ]\n        },\n        \"timestamp\": 1617045243181\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 123,\n                    \"y\": 74,\n                    \"id\": 45,\n                    \"timeOffset\": 0\n                }\n            ]\n        },\n        \"timestamp\": 1617045243717\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 118,\n                    \"y\": 74,\n                    \"id\": 45,\n                    \"timeOffset\": -441\n                },\n                {\n                    \"x\": 98,\n                    \"y\": 83,\n                    \"id\": 45,\n                    \"timeOffset\": -391\n                },\n                {\n                    \"x\": 81,\n                    \"y\": 90,\n                    \"id\": 48,\n                    \"timeOffset\": -341\n                },\n                {\n                    \"x\": 72,\n                    \"y\": 93,\n                    \"id\": 48,\n                    \"timeOffset\": -291\n                },\n                {\n                    \"x\": 66,\n                    \"y\": 95,\n                    \"id\": 48,\n                    \"timeOffset\": -241\n                },\n                {\n                    \"x\": 61,\n                    \"y\": 96,\n                    \"id\": 48,\n                    \"timeOffset\": -191\n                },\n                {\n                    \"x\": 58,\n                    \"y\": 96,\n                    \"id\": 48,\n                    \"timeOffset\": -141\n                },\n                {\n                    \"x\": 57,\n                    \"y\": 96,\n                    \"id\": 48,\n                    \"timeOffset\": -91\n                },\n                {\n                    \"x\": 56,\n                    \"y\": 96,\n                    \"id\": 48,\n                    \"timeOffset\": -41\n                }\n            ]\n        },\n        \"timestamp\": 1617045244217\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 55,\n                    \"y\": 95,\n                    \"id\": 48,\n                    \"timeOffset\": -497\n                },\n                {\n                    \"x\": 54,\n                    \"y\": 92,\n                    \"id\": 48,\n                    \"timeOffset\": -447\n                },\n                {\n                    \"x\": 54,\n                    \"y\": 90,\n                    \"id\": 48,\n                    \"timeOffset\": -380\n                },\n                {\n                    \"x\": 54,\n                    \"y\": 89,\n                    \"id\": 48,\n                    \"timeOffset\": -314\n                },\n                {\n                    \"x\": 54,\n                    \"y\": 88,\n                    \"id\": 48,\n                    \"timeOffset\": -247\n                },\n                {\n                    \"x\": 54,\n                    \"y\": 88,\n                    \"id\": 48,\n                    \"timeOffset\": -164\n                }\n            ]\n        },\n        \"timestamp\": 1617045244723\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045244758\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 5,\n            \"id\": 48\n        },\n        \"timestamp\": 1617045244759\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045244837\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045244837\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [\n                {\n                    \"parentId\": 44,\n                    \"id\": 53\n                }\n            ],\n            \"adds\": []\n        },\n        \"timestamp\": 1617045244841\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045245290\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045245418\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045245418\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [],\n            \"adds\": [\n                {\n                    \"parentId\": 44,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"div\",\n                        \"attributes\": {\n                            \"class\": \"bm bn\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 55\n                    }\n                },\n                {\n                    \"parentId\": 55,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 3,\n                        \"textContent\": \"test qweqwe\",\n                        \"id\": 56\n                    }\n                }\n            ]\n        },\n        \"timestamp\": 1617045245421\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 54,\n                    \"y\": 88,\n                    \"id\": 48,\n                    \"timeOffset\": 0\n                }\n            ]\n        },\n        \"timestamp\": 1617045246425\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045248579\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045248725\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045248725\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [\n                {\n                    \"parentId\": 44,\n                    \"id\": 55\n                }\n            ],\n            \"adds\": []\n        },\n        \"timestamp\": 1617045248728\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045250350\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045250463\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045250463\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [],\n            \"adds\": [\n                {\n                    \"parentId\": 44,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"div\",\n                        \"attributes\": {\n                            \"class\": \"bm bn\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 57\n                    }\n                },\n                {\n                    \"parentId\": 57,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 3,\n                        \"textContent\": \"test qweqwe\",\n                        \"id\": 58\n                    }\n                }\n            ]\n        },\n        \"timestamp\": 1617045250466\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045250795\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045250932\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045250932\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [\n                {\n                    \"parentId\": 44,\n                    \"id\": 57\n                }\n            ],\n            \"adds\": []\n        },\n        \"timestamp\": 1617045250934\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 4,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045250934\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045252276\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045252403\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045252403\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [],\n            \"adds\": [\n                {\n                    \"parentId\": 44,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 2,\n                        \"tagName\": \"div\",\n                        \"attributes\": {\n                            \"class\": \"bm bn\"\n                        },\n                        \"childNodes\": [],\n                        \"id\": 59\n                    }\n                },\n                {\n                    \"parentId\": 59,\n                    \"nextId\": null,\n                    \"node\": {\n                        \"type\": 3,\n                        \"textContent\": \"test qweqwe\",\n                        \"id\": 60\n                    }\n                }\n            ]\n        },\n        \"timestamp\": 1617045252406\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045252630\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045252730\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045252730\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 0,\n            \"texts\": [],\n            \"attributes\": [],\n            \"removes\": [\n                {\n                    \"parentId\": 44,\n                    \"id\": 59\n                }\n            ],\n            \"adds\": []\n        },\n        \"timestamp\": 1617045252732\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 4,\n            \"id\": 48,\n            \"x\": 54,\n            \"y\": 88\n        },\n        \"timestamp\": 1617045252733\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 55,\n                    \"y\": 88,\n                    \"id\": 48,\n                    \"timeOffset\": 0\n                }\n            ]\n        },\n        \"timestamp\": 1617045253507\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 59,\n                    \"y\": 86,\n                    \"id\": 48,\n                    \"timeOffset\": -450\n                },\n                {\n                    \"x\": 77,\n                    \"y\": 81,\n                    \"id\": 48,\n                    \"timeOffset\": -384\n                },\n                {\n                    \"x\": 116,\n                    \"y\": 76,\n                    \"id\": 45,\n                    \"timeOffset\": -334\n                },\n                {\n                    \"x\": 184,\n                    \"y\": 76,\n                    \"id\": 45,\n                    \"timeOffset\": -283\n                },\n                {\n                    \"x\": 234,\n                    \"y\": 81,\n                    \"id\": 45,\n                    \"timeOffset\": -217\n                },\n                {\n                    \"x\": 240,\n                    \"y\": 84,\n                    \"id\": 45,\n                    \"timeOffset\": -167\n                }\n            ]\n        },\n        \"timestamp\": 1617045254010\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 240,\n                    \"y\": 84,\n                    \"id\": 45,\n                    \"timeOffset\": -9\n                }\n            ]\n        },\n        \"timestamp\": 1617045254510\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 236,\n                    \"y\": 87,\n                    \"id\": 45,\n                    \"timeOffset\": -453\n                },\n                {\n                    \"x\": 217,\n                    \"y\": 104,\n                    \"id\": 19,\n                    \"timeOffset\": -401\n                },\n                {\n                    \"x\": 157,\n                    \"y\": 211,\n                    \"id\": 19,\n                    \"timeOffset\": -335\n                },\n                {\n                    \"x\": 128,\n                    \"y\": 264,\n                    \"id\": 19,\n                    \"timeOffset\": -285\n                },\n                {\n                    \"x\": 113,\n                    \"y\": 275,\n                    \"id\": 19,\n                    \"timeOffset\": -219\n                },\n                {\n                    \"x\": 103,\n                    \"y\": 234,\n                    \"id\": 19,\n                    \"timeOffset\": -168\n                },\n                {\n                    \"x\": 91,\n                    \"y\": 126,\n                    \"id\": 19,\n                    \"timeOffset\": -119\n                },\n                {\n                    \"x\": 85,\n                    \"y\": 60,\n                    \"id\": 45,\n                    \"timeOffset\": -69\n                },\n                {\n                    \"x\": 81,\n                    \"y\": 24,\n                    \"id\": 44,\n                    \"timeOffset\": -19\n                }\n            ]\n        },\n        \"timestamp\": 1617045255011\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 1,\n            \"id\": 46,\n            \"x\": 55,\n            \"y\": 19\n        },\n        \"timestamp\": 1617045255435\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 6,\n            \"id\": 48\n        },\n        \"timestamp\": 1617045255435\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 5,\n            \"id\": 46\n        },\n        \"timestamp\": 1617045255436\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 1,\n            \"positions\": [\n                {\n                    \"x\": 75,\n                    \"y\": 3,\n                    \"id\": 2,\n                    \"timeOffset\": -470\n                },\n                {\n                    \"x\": 66,\n                    \"y\": 0,\n                    \"id\": 2,\n                    \"timeOffset\": -276\n                },\n                {\n                    \"x\": 62,\n                    \"y\": 6,\n                    \"id\": 2,\n                    \"timeOffset\": -220\n                },\n                {\n                    \"x\": 58,\n                    \"y\": 14,\n                    \"id\": 46,\n                    \"timeOffset\": -169\n                },\n                {\n                    \"x\": 56,\n                    \"y\": 17,\n                    \"id\": 46,\n                    \"timeOffset\": -119\n                }\n            ]\n        },\n        \"timestamp\": 1617045255512\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 0,\n            \"id\": 46,\n            \"x\": 55,\n            \"y\": 19\n        },\n        \"timestamp\": 1617045255540\n    },\n    {\n        \"type\": 3,\n        \"data\": {\n            \"source\": 2,\n            \"type\": 2,\n            \"id\": 46,\n            \"x\": 55,\n            \"y\": 19\n        },\n        \"timestamp\": 1617045255540\n    }\n]"
  },
  {
    "path": "ui/src/components/sessions/session-viewer/example-events/test.json",
    "content": "[\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 0,\n            \"data\": {},\n            \"timestamp\": 1731633884348\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 4,\n            \"data\": {\n                \"href\": \"https://github.com/steel-dev/steel-browser\",\n                \"width\": 1920,\n                \"height\": 1080\n            },\n            \"timestamp\": 1731633884557\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 1,\n            \"data\": {},\n            \"timestamp\": 1731633884557\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 3,\n            \"data\": {\n                \"source\": 0,\n                \"texts\": [],\n                \"attributes\": [],\n                \"removes\": [],\n                \"adds\": [\n                    {\n                        \"parentId\": 1392,\n                        \"nextId\": 1395,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"style\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1393,\n                            \"isShadow\": true\n                        }\n                    },\n                    {\n                        \"parentId\": 1393,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 3,\n                            \"textContent\": \":host { display: block; }\",\n                            \"isStyle\": true,\n                            \"id\": 1394\n                        }\n                    },\n                    {\n                        \"parentId\": 1392,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"slot\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1395,\n                            \"isShadow\": true\n                        }\n                    }\n                ]\n            },\n            \"timestamp\": 1731633884648\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 3,\n            \"data\": {\n                \"source\": 0,\n                \"texts\": [],\n                \"attributes\": [],\n                \"removes\": [],\n                \"adds\": [\n                    {\n                        \"parentId\": 1451,\n                        \"nextId\": 1455,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"style\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1453,\n                            \"isShadow\": true\n                        }\n                    },\n                    {\n                        \"parentId\": 1453,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 3,\n                            \"textContent\": \":host { --tooltip-top: var(--tool-tip-position-top, 0); --tooltip-left: var(--tool-tip-position-left, 0); font: var(--text-body-shorthand-small); text-align: center; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; border-radius: var(--borderRadius-medium); opacity: 0; max-width: var(--overlay-width-small); overflow-wrap: break-word; white-space: normal; text-wrap-style: balance; padding: var(--overlay-paddingBlock-condensed) var(--overlay-padding-condensed) !important; color: var(--tooltip-fgColor, var(--fgColor-onEmphasis)) !important; background: var(--tooltip-bgColor, var(--bgColor-emphasis)) !important; border: 0px !important; width: max-content !important; inset: var(--tooltip-top) auto auto var(--tooltip-left) !important; overflow: visible !important; }:host(:is(.tooltip-n, .tooltip-nw, .tooltip-ne)) { --tooltip-top: calc(var(--tool-tip-position-top, 0) - var(--overlay-offset, 0.25rem)); --tooltip-left: var(--tool-tip-position-left); }:host(:is(.tooltip-s, .tooltip-sw, .tooltip-se)) { --tooltip-top: calc(var(--tool-tip-position-top, 0) + var(--overlay-offset, 0.25rem)); --tooltip-left: var(--tool-tip-position-left); }:host(.tooltip-w) { --tooltip-top: var(--tool-tip-position-top); --tooltip-left: calc(var(--tool-tip-position-left, 0) - var(--overlay-offset, 0.25rem)); }:host(.tooltip-e) { --tooltip-top: var(--tool-tip-position-top); --tooltip-left: calc(var(--tool-tip-position-left, 0) + var(--overlay-offset, 0.25rem)); }:host::after { position: absolute; display: block; right: 0px; left: 0px; height: var(--overlay-offset, 0.25rem); content: \\\"\\\"; }:host(.tooltip-s)::after, :host(.tooltip-se)::after, :host(.tooltip-sw)::after { bottom: 100%; }:host(.tooltip-n)::after, :host(.tooltip-ne)::after, :host(.tooltip-nw)::after { top: 100%; }@keyframes tooltip-appear { \\n  0% { opacity: 0; }\\n  100% { opacity: 1; }\\n}:host(:popover-open), :host(:popover-open)::before { animation-name: tooltip-appear; animation-duration: 0.1s; animation-fill-mode: forwards; animation-timing-function: ease-in; }:host(.\\\\:popover-open) { animation-name: tooltip-appear; animation-duration: 0.1s; animation-fill-mode: forwards; animation-timing-function: ease-in; }@media (forced-colors: active) {\\n  :host { outline: transparent solid 1px; }\\n  :host::before { display: none; }\\n}\",\n                            \"isStyle\": true,\n                            \"id\": 1454\n                        }\n                    },\n                    {\n                        \"parentId\": 1451,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"slot\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1455,\n                            \"isShadow\": true\n                        }\n                    }\n                ]\n            },\n            \"timestamp\": 1731633884649\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 3,\n            \"data\": {\n                \"source\": 0,\n                \"texts\": [],\n                \"attributes\": [],\n                \"removes\": [],\n                \"adds\": [\n                    {\n                        \"parentId\": 1752,\n                        \"nextId\": 1756,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"style\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1754,\n                            \"isShadow\": true\n                        }\n                    },\n                    {\n                        \"parentId\": 1754,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 3,\n                            \"textContent\": \":host { --tooltip-top: var(--tool-tip-position-top, 0); --tooltip-left: var(--tool-tip-position-left, 0); font: var(--text-body-shorthand-small); text-align: center; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; border-radius: var(--borderRadius-medium); opacity: 0; max-width: var(--overlay-width-small); overflow-wrap: break-word; white-space: normal; text-wrap-style: balance; padding: var(--overlay-paddingBlock-condensed) var(--overlay-padding-condensed) !important; color: var(--tooltip-fgColor, var(--fgColor-onEmphasis)) !important; background: var(--tooltip-bgColor, var(--bgColor-emphasis)) !important; border: 0px !important; width: max-content !important; inset: var(--tooltip-top) auto auto var(--tooltip-left) !important; overflow: visible !important; }:host(:is(.tooltip-n, .tooltip-nw, .tooltip-ne)) { --tooltip-top: calc(var(--tool-tip-position-top, 0) - var(--overlay-offset, 0.25rem)); --tooltip-left: var(--tool-tip-position-left); }:host(:is(.tooltip-s, .tooltip-sw, .tooltip-se)) { --tooltip-top: calc(var(--tool-tip-position-top, 0) + var(--overlay-offset, 0.25rem)); --tooltip-left: var(--tool-tip-position-left); }:host(.tooltip-w) { --tooltip-top: var(--tool-tip-position-top); --tooltip-left: calc(var(--tool-tip-position-left, 0) - var(--overlay-offset, 0.25rem)); }:host(.tooltip-e) { --tooltip-top: var(--tool-tip-position-top); --tooltip-left: calc(var(--tool-tip-position-left, 0) + var(--overlay-offset, 0.25rem)); }:host::after { position: absolute; display: block; right: 0px; left: 0px; height: var(--overlay-offset, 0.25rem); content: \\\"\\\"; }:host(.tooltip-s)::after, :host(.tooltip-se)::after, :host(.tooltip-sw)::after { bottom: 100%; }:host(.tooltip-n)::after, :host(.tooltip-ne)::after, :host(.tooltip-nw)::after { top: 100%; }@keyframes tooltip-appear { \\n  0% { opacity: 0; }\\n  100% { opacity: 1; }\\n}:host(:popover-open), :host(:popover-open)::before { animation-name: tooltip-appear; animation-duration: 0.1s; animation-fill-mode: forwards; animation-timing-function: ease-in; }:host(.\\\\:popover-open) { animation-name: tooltip-appear; animation-duration: 0.1s; animation-fill-mode: forwards; animation-timing-function: ease-in; }@media (forced-colors: active) {\\n  :host { outline: transparent solid 1px; }\\n  :host::before { display: none; }\\n}\",\n                            \"isStyle\": true,\n                            \"id\": 1755\n                        }\n                    },\n                    {\n                        \"parentId\": 1752,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"slot\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1756,\n                            \"isShadow\": true\n                        }\n                    }\n                ]\n            },\n            \"timestamp\": 1731633884649\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 3,\n            \"data\": {\n                \"source\": 0,\n                \"texts\": [],\n                \"attributes\": [],\n                \"removes\": [],\n                \"adds\": [\n                    {\n                        \"parentId\": 3609,\n                        \"nextId\": 3613,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"style\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 3611,\n                            \"isShadow\": true\n                        }\n                    },\n                    {\n                        \"parentId\": 3611,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 3,\n                            \"textContent\": \":host { display: block; }\",\n                            \"isStyle\": true,\n                            \"id\": 3612\n                        }\n                    },\n                    {\n                        \"parentId\": 3609,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"slot\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 3613,\n                            \"isShadow\": true\n                        }\n                    }\n                ]\n            },\n            \"timestamp\": 1731633884649\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 3,\n            \"data\": {\n                \"source\": 0,\n                \"texts\": [],\n                \"attributes\": [],\n                \"removes\": [],\n                \"adds\": [\n                    {\n                        \"parentId\": 1613,\n                        \"nextId\": 1617,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"style\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1615,\n                            \"isShadow\": true\n                        }\n                    },\n                    {\n                        \"parentId\": 1615,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 3,\n                            \"textContent\": \":host { --tooltip-top: var(--tool-tip-position-top, 0); --tooltip-left: var(--tool-tip-position-left, 0); font: var(--text-body-shorthand-small); text-align: center; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; border-radius: var(--borderRadius-medium); opacity: 0; max-width: var(--overlay-width-small); overflow-wrap: break-word; white-space: normal; text-wrap-style: balance; padding: var(--overlay-paddingBlock-condensed) var(--overlay-padding-condensed) !important; color: var(--tooltip-fgColor, var(--fgColor-onEmphasis)) !important; background: var(--tooltip-bgColor, var(--bgColor-emphasis)) !important; border: 0px !important; width: max-content !important; inset: var(--tooltip-top) auto auto var(--tooltip-left) !important; overflow: visible !important; }:host(:is(.tooltip-n, .tooltip-nw, .tooltip-ne)) { --tooltip-top: calc(var(--tool-tip-position-top, 0) - var(--overlay-offset, 0.25rem)); --tooltip-left: var(--tool-tip-position-left); }:host(:is(.tooltip-s, .tooltip-sw, .tooltip-se)) { --tooltip-top: calc(var(--tool-tip-position-top, 0) + var(--overlay-offset, 0.25rem)); --tooltip-left: var(--tool-tip-position-left); }:host(.tooltip-w) { --tooltip-top: var(--tool-tip-position-top); --tooltip-left: calc(var(--tool-tip-position-left, 0) - var(--overlay-offset, 0.25rem)); }:host(.tooltip-e) { --tooltip-top: var(--tool-tip-position-top); --tooltip-left: calc(var(--tool-tip-position-left, 0) + var(--overlay-offset, 0.25rem)); }:host::after { position: absolute; display: block; right: 0px; left: 0px; height: var(--overlay-offset, 0.25rem); content: \\\"\\\"; }:host(.tooltip-s)::after, :host(.tooltip-se)::after, :host(.tooltip-sw)::after { bottom: 100%; }:host(.tooltip-n)::after, :host(.tooltip-ne)::after, :host(.tooltip-nw)::after { top: 100%; }@keyframes tooltip-appear { \\n  0% { opacity: 0; }\\n  100% { opacity: 1; }\\n}:host(:popover-open), :host(:popover-open)::before { animation-name: tooltip-appear; animation-duration: 0.1s; animation-fill-mode: forwards; animation-timing-function: ease-in; }:host(.\\\\:popover-open) { animation-name: tooltip-appear; animation-duration: 0.1s; animation-fill-mode: forwards; animation-timing-function: ease-in; }@media (forced-colors: active) {\\n  :host { outline: transparent solid 1px; }\\n  :host::before { display: none; }\\n}\",\n                            \"isStyle\": true,\n                            \"id\": 1616\n                        }\n                    },\n                    {\n                        \"parentId\": 1613,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"slot\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1617,\n                            \"isShadow\": true\n                        }\n                    }\n                ]\n            },\n            \"timestamp\": 1731633884649\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 3,\n            \"data\": {\n                \"source\": 0,\n                \"texts\": [],\n                \"attributes\": [],\n                \"removes\": [],\n                \"adds\": [\n                    {\n                        \"parentId\": 3478,\n                        \"nextId\": 3482,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"style\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 3480,\n                            \"isShadow\": true\n                        }\n                    },\n                    {\n                        \"parentId\": 3480,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 3,\n                            \"textContent\": \":host { display: block; }\",\n                            \"isStyle\": true,\n                            \"id\": 3481\n                        }\n                    },\n                    {\n                        \"parentId\": 3478,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"slot\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 3482,\n                            \"isShadow\": true\n                        }\n                    }\n                ]\n            },\n            \"timestamp\": 1731633884649\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 3,\n            \"data\": {\n                \"source\": 0,\n                \"texts\": [],\n                \"attributes\": [],\n                \"removes\": [],\n                \"adds\": [\n                    {\n                        \"parentId\": 1377,\n                        \"nextId\": 1381,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"style\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1379,\n                            \"isShadow\": true\n                        }\n                    },\n                    {\n                        \"parentId\": 1379,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 3,\n                            \"textContent\": \":host { --tooltip-top: var(--tool-tip-position-top, 0); --tooltip-left: var(--tool-tip-position-left, 0); font: var(--text-body-shorthand-small); text-align: center; text-decoration: none; text-shadow: none; text-transform: none; letter-spacing: normal; border-radius: var(--borderRadius-medium); opacity: 0; max-width: var(--overlay-width-small); overflow-wrap: break-word; white-space: normal; text-wrap-style: balance; padding: var(--overlay-paddingBlock-condensed) var(--overlay-padding-condensed) !important; color: var(--tooltip-fgColor, var(--fgColor-onEmphasis)) !important; background: var(--tooltip-bgColor, var(--bgColor-emphasis)) !important; border: 0px !important; width: max-content !important; inset: var(--tooltip-top) auto auto var(--tooltip-left) !important; overflow: visible !important; }:host(:is(.tooltip-n, .tooltip-nw, .tooltip-ne)) { --tooltip-top: calc(var(--tool-tip-position-top, 0) - var(--overlay-offset, 0.25rem)); --tooltip-left: var(--tool-tip-position-left); }:host(:is(.tooltip-s, .tooltip-sw, .tooltip-se)) { --tooltip-top: calc(var(--tool-tip-position-top, 0) + var(--overlay-offset, 0.25rem)); --tooltip-left: var(--tool-tip-position-left); }:host(.tooltip-w) { --tooltip-top: var(--tool-tip-position-top); --tooltip-left: calc(var(--tool-tip-position-left, 0) - var(--overlay-offset, 0.25rem)); }:host(.tooltip-e) { --tooltip-top: var(--tool-tip-position-top); --tooltip-left: calc(var(--tool-tip-position-left, 0) + var(--overlay-offset, 0.25rem)); }:host::after { position: absolute; display: block; right: 0px; left: 0px; height: var(--overlay-offset, 0.25rem); content: \\\"\\\"; }:host(.tooltip-s)::after, :host(.tooltip-se)::after, :host(.tooltip-sw)::after { bottom: 100%; }:host(.tooltip-n)::after, :host(.tooltip-ne)::after, :host(.tooltip-nw)::after { top: 100%; }@keyframes tooltip-appear { \\n  0% { opacity: 0; }\\n  100% { opacity: 1; }\\n}:host(:popover-open), :host(:popover-open)::before { animation-name: tooltip-appear; animation-duration: 0.1s; animation-fill-mode: forwards; animation-timing-function: ease-in; }:host(.\\\\:popover-open) { animation-name: tooltip-appear; animation-duration: 0.1s; animation-fill-mode: forwards; animation-timing-function: ease-in; }@media (forced-colors: active) {\\n  :host { outline: transparent solid 1px; }\\n  :host::before { display: none; }\\n}\",\n                            \"isStyle\": true,\n                            \"id\": 1380\n                        }\n                    },\n                    {\n                        \"parentId\": 1377,\n                        \"nextId\": null,\n                        \"node\": {\n                            \"type\": 2,\n                            \"tagName\": \"slot\",\n                            \"attributes\": {},\n                            \"childNodes\": [],\n                            \"id\": 1381,\n                            \"isShadow\": true\n                        }\n                    }\n                ]\n            },\n            \"timestamp\": 1731633884649\n        }\n    },\n    {\n        \"pageId\": \"71C110B22AC301E9F0EAD1580204D6D1\",\n        \"event\": {\n            \"type\": 3,\n            \"data\": {\n                \"source\": 0,\n                \"texts\": [],\n                \"attributes\": [\n                    {\n                        \"id\": 1770,\n                        \"attributes\": {\n                            \"hidden\": \"\"\n                        }\n                    },\n                    {\n                        \"id\": 1787,\n                        \"attributes\": {\n                            \"hidden\": \"\"\n                        }\n                    },\n                    {\n                        \"id\": 1805,\n                        \"attributes\": {\n                            \"hidden\": \"\"\n                        }\n                    },\n                    {\n                        \"id\": 1822,\n                        \"attributes\": {\n                            \"hidden\": \"\"\n                        }\n                    },\n                    {\n                        \"id\": 1839,\n                        \"attributes\": {\n                            \"hidden\": \"\"\n                        }\n                    },\n                    {\n                        \"id\": 1856,\n                        \"attributes\": {\n                            \"hidden\": \"\"\n                        }\n                    },\n                    {\n                        \"id\": 1873,\n                        \"attributes\": {\n                            \"hidden\": \"\"\n                        }\n                    }\n                ],\n                \"removes\": [],\n                \"adds\": []\n            },\n            \"timestamp\": 1731633884652\n        }\n    }\n]"
  },
  {
    "path": "ui/src/components/sessions/session-viewer/index.tsx",
    "content": "export { SessionViewer } from \"./session-viewer\";\n"
  },
  {
    "path": "ui/src/components/sessions/session-viewer/live-empty-state.tsx",
    "content": "import { Globe } from \"@/components/illustrations/globe\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { copyText } from \"@/utils/toasts\";\nimport { CopyIcon } from \"@radix-ui/react-icons\";\nimport { SessionDetails } from \"@/steel-client\";\n\nexport function LiveEmptyState({ session }: { session: SessionDetails }) {\n  return (\n    <div className=\"flex flex-col w-full flex-1 px-24 pt-10 bg-[url('/grid.svg')] bg-no-repeat bg-center border-[var(--gray-6)] bg-cover items-center\">\n      <div className=\"flex flex-col gap-4 max-w-[396px] mx-auto\">\n        <div className=\"flex flex-col gap-4 mx-auto\">\n          <h1 className=\"text-primary text-2xl  font-medium text-center\">\n            Your session is live!\n          </h1>\n          <p className=\"text-muted-foreground text-lg text-center max-w-[280px]\">\n            You'll be able to view and monitor it from here.\n          </p>\n        </div>\n        <div className=\"flex justify-center items-center w-full\">\n          <Globe />\n        </div>\n        <div className=\"flex flex-col gap-2 justify-center items-center\">\n          <p className=\"text-muted-foreground text-lg text-center max-w-[280px]\">\n            Connect via this websocket URL\n          </p>\n          <Badge\n            variant=\"secondary\"\n            className=\"text-primary bg-[var(--gray-3)] py-2 px-3 flex items-center justify-between min-w-[400px] max-w-[700px] hover:bg-[var(--gray-4)] ml-auto\"\n          >\n            <span className=\"text-xs text-muted-foreground font-regular\">\n              {session?.websocketUrl.substring(0, 50)}\n            </span>\n            <div\n              className=\"flex items-center ml-auto hover:cursor-pointer\"\n              onClick={() =>\n                copyText(session?.websocketUrl || \"\", \"Websocket URL\")\n              }\n            >\n              <CopyIcon className=\"w-4 h-4\" />\n            </div>\n          </Badge>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/sessions/session-viewer/session-viewer-controls.css",
    "content": ".replayer-wrapper {\n  position: relative;\n}\n\n.replayer-mouse {\n  position: absolute;\n  width: 20px;\n  height: 20px;\n  transition:\n    left 0.05s linear,\n    top 0.05s linear;\n  background-size: contain;\n  background-position: 50%;\n  background-repeat: no-repeat;\n  background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScyNCcgaGVpZ2h0PScyNicgZmlsbD0nbm9uZScgdmlld0JveD0nMCAwIDI0IDI2Jz48cGF0aCBmaWxsLXJ1bGU9J2V2ZW5vZGQnIGNsaXAtcnVsZT0nZXZlbm9kZCcgZD0nTTEuNjkgMi42OWEyLjM1NyAyLjM1NyAwIDAgMSAyLjQ5NS0uNTRMMjEuNDcgOC42MzJhMi4zNTcgMi4zNTcgMCAwIDEtLjI1NSA0LjQ5NGwtNy4yNzEgMS44MTgtMS44MTggNy4yN2EyLjM1NyAyLjM1NyAwIDAgMS00LjQ5NC4yNTZMMS4xNSA1LjE4NWEyLjM1NyAyLjM1NyAwIDAgMSAuNTQtMi40OTVaJyBmaWxsPScjZmZmJy8+PHBhdGggZmlsbC1ydWxlPSdldmVub2RkJyBjbGlwLXJ1bGU9J2V2ZW5vZGQnIGQ9J00zLjYzMyAzLjYyMkEuNzg2Ljc4NiAwIDAgMCAyLjYyIDQuNjMzTDkuMTAzIDIxLjkyYS43ODYuNzg2IDAgMCAwIDEuNDk4LS4wODZsMi4wNDctOC4xODUgOC4xODUtMi4wNDZhLjc4NS43ODUgMCAwIDAgLjA4Ni0xLjQ5OEwzLjYzMyAzLjYyMlonIGZpbGw9JyMwMTAxMDEnLz48L3N2Zz4=\");\n  border-color: transparent;\n}\n\n.replayer-mouse:after {\n  content: \"\";\n  display: inline-block;\n  width: 20px;\n  height: 20px;\n  background: #4950f6;\n  border-radius: 100%;\n  transform: translate(-50%, -50%);\n  opacity: 0.3;\n}\n\n.replayer-mouse.active:after {\n  animation: click 0.2s ease-in-out 1;\n}\n\n.replayer-mouse.touch-device {\n  background-image: none;\n  width: 70px;\n  height: 70px;\n  border-radius: 100%;\n  margin-left: -37px;\n  margin-top: -37px;\n  border: 4px solid rgba(73, 80, 246, 0);\n  transition:\n    left 0s linear,\n    top 0s linear,\n    border-color 0.2s ease-in-out;\n}\n\n.replayer-mouse.touch-device.touch-active {\n  border-color: #4950f6;\n  transition:\n    left 0.25s linear,\n    top 0.25s linear,\n    border-color 0.2s ease-in-out;\n}\n\n.replayer-mouse.touch-device:after {\n  opacity: 0;\n}\n\n.replayer-mouse.touch-device.active:after {\n  animation: touch-click 0.2s ease-in-out 1;\n}\n\n.replayer-mouse-tail {\n  position: absolute;\n  pointer-events: none;\n}\n\n@keyframes click {\n  0% {\n    opacity: 0.3;\n    width: 20px;\n    height: 20px;\n  }\n\n  50% {\n    opacity: 0.5;\n    width: 10px;\n    height: 10px;\n  }\n}\n\n@keyframes touch-click {\n  0% {\n    opacity: 0;\n    width: 20px;\n    height: 20px;\n  }\n\n  50% {\n    opacity: 0.5;\n    width: 10px;\n    height: 10px;\n  }\n}\n\n.rr-player {\n  position: relative;\n  background: var(--gray-2);\n  float: left;\n  border-radius: 0 0 5px 5px;\n  width: 100%;\n  height: auto;\n  /* box-shadow: 0 24px 48px rgba(17, 16, 62, 0.12); */\n}\n\n.rr-player__frame {\n  overflow: hidden;\n  width: 100%;\n  height: auto;\n}\n\n.replayer-wrapper {\n  float: left;\n  clear: both;\n  transform-origin: top left;\n  left: 50%;\n  top: 50%;\n}\n\n.replayer-wrapper > iframe {\n  border: none;\n}\n\n.rr-controller.svelte-19ke1iv.svelte-19ke1iv {\n  width: 100%;\n  height: 80px;\n  background: var(--gray-2) !important;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-around;\n  align-items: center;\n  border-radius: 0 0 5px 5px;\n  border-top: solid 1px var(--gray-6);\n}\n\n.rr-timeline.svelte-19ke1iv.svelte-19ke1iv {\n  width: 80%;\n  display: flex;\n  align-items: center;\n}\n\n.rr-timeline__time.svelte-19ke1iv.svelte-19ke1iv {\n  display: inline-block;\n  width: 100px;\n  text-align: center;\n  color: #fff;\n}\n\n.rr-progress.svelte-19ke1iv.svelte-19ke1iv {\n  flex: 1;\n  height: 12px;\n  background: #27272a;\n  position: relative;\n  border-radius: 8px;\n  cursor: pointer;\n  box-sizing: border-box;\n  border-top: solid 1px transparent;\n  border-bottom: solid 1px transparent;\n}\n\n.rr-progress.disabled.svelte-19ke1iv.svelte-19ke1iv {\n  cursor: not-allowed;\n}\n\n.rr-progress__step.svelte-19ke1iv.svelte-19ke1iv {\n  height: 100%;\n  position: absolute;\n  left: 0;\n  top: 0;\n  background: #fff;\n  border-radius: 999px;\n}\n\n.rr-progress__handler.svelte-19ke1iv.svelte-19ke1iv {\n  width: 20px;\n  height: 20px;\n  border-radius: 999px;\n  position: absolute;\n  top: 2px;\n  transform: translate(-50%, -50%);\n}\n\n.rr-controller__btns.svelte-19ke1iv.svelte-19ke1iv {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 13px;\n}\n\n.rr-controller__btns.svelte-19ke1iv button.svelte-19ke1iv {\n  width: 32px;\n  height: 32px;\n  display: flex;\n  padding: 0;\n  align-items: center;\n  justify-content: center;\n  background: none;\n  border: none;\n  border-radius: 5px;\n  cursor: pointer;\n  color: #fff;\n}\n\nbutton.svelte-19ke1iv > svg > path {\n  fill: #fff;\n}\n\nbutton.svelte-19ke1iv:active > svg > path {\n  fill: #27272a;\n}\n\n.rr-controller__btns.svelte-19ke1iv button.svelte-19ke1iv:active {\n  background: #27272a;\n  color: var(--gray-12);\n}\n\n.rr-controller__btns.svelte-19ke1iv button.active.svelte-19ke1iv {\n  background: #27272a;\n  color: var(--gray-12);\n}\n\n.rr-controller__btns.svelte-19ke1iv button.svelte-19ke1iv:disabled {\n  cursor: not-allowed;\n}\n\n.switch.svelte-9brlez.svelte-9brlez.svelte-9brlez {\n  height: 1em;\n  display: flex;\n  align-items: center;\n}\n\n.switch.disabled.svelte-9brlez.svelte-9brlez.svelte-9brlez {\n  opacity: 0.5;\n}\n\n.label.svelte-9brlez.svelte-9brlez.svelte-9brlez {\n  margin: 0 8px;\n}\n\n.switch.svelte-9brlez input[type=\"checkbox\"].svelte-9brlez.svelte-9brlez {\n  position: absolute;\n  opacity: 0;\n}\n\n.switch.svelte-9brlez label.svelte-9brlez.svelte-9brlez {\n  width: 2em;\n  height: 1em;\n  position: relative;\n  cursor: pointer;\n  display: block;\n}\n\n.switch.disabled.svelte-9brlez label.svelte-9brlez.svelte-9brlez {\n  cursor: not-allowed;\n}\n\n.switch.svelte-9brlez label.svelte-9brlez.svelte-9brlez:before {\n  content: \"\";\n  position: absolute;\n  width: 2em;\n  height: 1em;\n  left: 0.1em;\n  transition: background 0.1s ease;\n  background: #27272a;\n  border-radius: 50px;\n}\n\n.switch.svelte-9brlez label.svelte-9brlez.svelte-9brlez:after {\n  content: \"\";\n  position: absolute;\n  width: 1em;\n  height: 1em;\n  border-radius: 50px;\n  left: 0;\n  transition: all 0.2s ease;\n  box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.3);\n  background: var(--gray-12);\n  animation: switch-off 0.2s ease-out;\n  z-index: 2;\n}\n\n.switch.svelte-9brlez\n  input[type=\"checkbox\"].svelte-9brlez:checked\n  + label.svelte-9brlez:before {\n  background: var(--gray-12);\n}\n\n.switch.svelte-9brlez\n  input[type=\"checkbox\"].svelte-9brlez:checked\n  + label.svelte-9brlez:after {\n  animation: switch-on 0.2s ease-out;\n  left: 1.1em;\n}\n\nspan.label.svelte-9brlez {\n  color: var(--gray-12);\n}\n"
  },
  {
    "path": "ui/src/components/sessions/session-viewer/session-viewer.tsx",
    "content": "import { useSessionsContext } from \"@/hooks/use-sessions-context\";\nimport { useRef, useEffect, useCallback } from \"react\";\nimport \"./session-viewer-controls.css\";\nimport { LoadingSpinner } from \"@/components/icons/LoadingSpinner\";\n\ntype SessionViewerProps = {\n  id: string;\n};\n\nlet clipboardBridgeActive = false;\n\nexport function SessionViewer({ id }: SessionViewerProps) {\n  const { useSession } = useSessionsContext();\n  const {\n    data: session,\n    isLoading: isSessionLoading,\n    isError: isSessionError,\n  } = useSession(id);\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const iframeRef = useRef<HTMLIFrameElement>(null);\n\n  // Clipboard bridge message handler\n  const handleMessage = useCallback(async (event: MessageEvent) => {\n    if (\n      !iframeRef.current ||\n      event.source !== iframeRef.current.contentWindow\n    ) {\n      return;\n    }\n    try {\n      switch (event.data.type) {\n        case \"requestClipboardRead\":\n          try {\n            const text = await navigator.clipboard.readText();\n            iframeRef.current.contentWindow?.postMessage(\n              {\n                type: \"clipboardReadResponse\",\n                text: text,\n                requestId: event.data.requestId,\n              },\n              \"*\",\n            );\n          } catch (error) {\n            iframeRef.current.contentWindow?.postMessage(\n              {\n                type: \"clipboardReadResponse\",\n                error: \"Failed to read clipboard\",\n                requestId: event.data.requestId,\n              },\n              \"*\",\n            );\n          }\n          break;\n\n        case \"requestClipboardWrite\":\n          try {\n            await navigator.clipboard.writeText(event.data.text);\n            iframeRef.current.contentWindow?.postMessage(\n              {\n                type: \"clipboardWriteResponse\",\n                success: true,\n                requestId: event.data.requestId,\n              },\n              \"*\",\n            );\n          } catch (error) {\n            iframeRef.current.contentWindow?.postMessage(\n              {\n                type: \"clipboardWriteResponse\",\n                success: false,\n                error: \"Failed to write to clipboard\",\n                requestId: event.data.requestId,\n              },\n              \"*\",\n            );\n          }\n          break;\n\n        case \"clipboardBridgeReady\":\n          break;\n      }\n    } catch (error) {\n      console.error(\"Error handling clipboard bridge message:\", error);\n    }\n  }, []);\n\n  // Global keyboard event handler for copy/paste\n  const handleKeyDown = useCallback(async (event: KeyboardEvent) => {\n    // Only handle if our container is focused or contains the focused element\n    if (!containerRef.current?.contains(document.activeElement)) {\n      return;\n    }\n\n    const isCtrlOrCmd = event.ctrlKey || event.metaKey;\n\n    if (isCtrlOrCmd && (event.key === \"c\" || event.key === \"C\")) {\n      event.preventDefault();\n\n      // Try to trigger copy from iframe\n      if (iframeRef.current?.contentWindow) {\n        iframeRef.current.contentWindow.postMessage(\n          {\n            type: \"triggerCopy\",\n          },\n          \"*\",\n        );\n      }\n    } else if (isCtrlOrCmd && (event.key === \"v\" || event.key === \"V\")) {\n      event.preventDefault();\n\n      try {\n        const text = await navigator.clipboard.readText();\n\n        if (text && iframeRef.current?.contentWindow) {\n          iframeRef.current.contentWindow.postMessage(\n            {\n              type: \"triggerPaste\",\n              text: text,\n            },\n            \"*\",\n          );\n        }\n      } catch (error) {\n        console.error(\"Failed to read clipboard for paste:\", error);\n      }\n    }\n  }, []);\n\n  // Set up event listeners\n  useEffect(() => {\n    if (!clipboardBridgeActive) {\n      window.addEventListener(\"message\", handleMessage);\n      document.addEventListener(\"keydown\", handleKeyDown, true);\n      clipboardBridgeActive = true;\n    }\n\n    return () => {\n      // Don't remove global listeners - they should persist across component re-renders\n    };\n  }, [handleMessage, handleKeyDown]);\n\n  // Make container focusable and handle clicks\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const handleClick = () => {\n      container.focus();\n    };\n\n    container.addEventListener(\"click\", handleClick);\n    return () => container.removeEventListener(\"click\", handleClick);\n  }, []);\n\n  if (isSessionLoading)\n    return (\n      <div\n        ref={containerRef}\n        className=\"flex flex-col w-full flex-1 border-t border-[var(--gray-6)]\"\n      >\n        <div className=\"flex flex-col items-center justify-center flex-1 w-full\">\n          <LoadingSpinner className=\"w-16 h-16 text-[var(--gray-6)]\" />\n        </div>\n      </div>\n    );\n  if (isSessionError)\n    return (\n      <div\n        ref={containerRef}\n        className=\"flex flex-col w-full flex-1 border-t border-[var(--gray-6)]\"\n      >\n        <h1 className=\"text-[var(--tomato-5)]\">Error loading session</h1>\n      </div>\n    );\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"flex flex-col w-full overflow-hidden flex-1 border-t border-[var(--gray-6)]\"\n      tabIndex={0}\n      style={{ outline: \"none\" }}\n    >\n      <iframe\n        ref={iframeRef}\n        src={`${session?.debugUrl}${\n          session?.debugUrl?.includes(\"?\") ? \"&\" : \"?\"\n        }clipboardBridge=true`}\n        sandbox=\"allow-same-origin allow-scripts allow-clipboard-write allow-clipboard-read\"\n        className=\"w-full max-h-full aspect-[16/10] border border-[var(--gray-6)]\"\n        allow=\"clipboard-read; clipboard-write\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/theme-provider.tsx",
    "content": "import { createContext, useEffect, useState } from \"react\";\n\ntype Theme = \"dark\" | \"light\" | \"system\";\n\ntype ThemeProviderProps = {\n  children: React.ReactNode;\n  defaultTheme?: Theme;\n  storageKey?: string;\n};\n\ntype ThemeProviderState = {\n  theme: Theme;\n  setTheme: (theme: Theme) => void;\n};\n\nconst initialState: ThemeProviderState = {\n  theme: \"system\",\n  setTheme: () => null,\n};\n\nconst ThemeProviderContext = createContext<ThemeProviderState>(initialState);\n\nexport function ThemeProvider({\n  children,\n  defaultTheme = \"system\",\n  storageKey = \"steel-ui-theme\",\n  ...props\n}: ThemeProviderProps) {\n  const [theme, setTheme] = useState<Theme>(\n    () => (localStorage.getItem(storageKey) as Theme) || defaultTheme,\n  );\n\n  useEffect(() => {\n    const root = window.document.documentElement;\n\n    root.classList.remove(\"light\", \"dark\");\n\n    if (theme === \"system\") {\n      const systemTheme = window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n        ? \"dark\"\n        : \"light\";\n\n      root.classList.add(systemTheme);\n      return;\n    }\n\n    root.classList.add(theme);\n  }, [theme]);\n\n  const value = {\n    theme,\n    setTheme: (theme: Theme) => {\n      localStorage.setItem(storageKey, theme);\n      setTheme(theme);\n    },\n  };\n\n  return (\n    <ThemeProviderContext.Provider {...props} value={value}>\n      {children}\n    </ThemeProviderContext.Provider>\n  );\n}\n"
  },
  {
    "path": "ui/src/components/ui/avatar.tsx",
    "content": "import * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatar.displayName = AvatarPrimitive.Root.displayName\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n))\nAvatarImage.displayName = AvatarPrimitive.Image.displayName\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-muted\",\n      className\n    )}\n    {...props}\n  />\n))\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
  },
  {
    "path": "ui/src/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default: \"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return <div className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n\nexport { Badge };\n"
  },
  {
    "path": "ui/src/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground shadow hover:bg-primary/90\",\n        destructive: \"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground\",\n        secondary: \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2\",\n        sm: \"h-8 rounded-md px-3 text-xs\",\n        lg: \"h-10 rounded-md px-8\",\n        icon: \"h-9 w-9\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\n// eslint-disable-next-line react-refresh/only-export-components\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "ui/src/components/ui/card.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-xl border bg-card text-card-foreground shadow\",\n      className\n    )}\n    {...props}\n  />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h3\n    ref={ref}\n    className={cn(\"font-semibold leading-none tracking-tight\", className)}\n    {...props}\n  />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n))\nCardFooter.displayName = \"CardFooter\"\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }\n"
  },
  {
    "path": "ui/src/components/ui/checkbox.tsx",
    "content": "import * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { CheckIcon } from \"@radix-ui/react-icons\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n      className\n    )}\n    {...props}\n  >\n    <CheckboxPrimitive.Indicator\n      className={cn(\"flex items-center justify-center text-current\")}\n    >\n      <CheckIcon className=\"h-4 w-4\" />\n    </CheckboxPrimitive.Indicator>\n  </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n"
  },
  {
    "path": "ui/src/components/ui/dialog.tsx",
    "content": "import * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { Cross2Icon } from \"@radix-ui/react-icons\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Dialog = DialogPrimitive.Root\n\nconst DialogTrigger = DialogPrimitive.Trigger\n\nconst DialogPortal = DialogPrimitive.Portal\n\nconst DialogClose = DialogPrimitive.Close\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <Cross2Icon className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n))\nDialogContent.displayName = DialogPrimitive.Content.displayName\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogHeader.displayName = \"DialogHeader\"\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nDialogFooter.displayName = \"DialogFooter\"\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n))\nDialogDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n}\n"
  },
  {
    "path": "ui/src/components/ui/form.tsx",
    "content": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  useFormContext,\n} from \"react-hook-form\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Label } from \"@/components/ui/label\";\n\nconst Form = FormProvider;\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField should be used within <FormField>\");\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);\n\nconst FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, ...props }, ref) => {\n    const id = React.useId();\n\n    return (\n      <FormItemContext.Provider value={{ id }}>\n        <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n      </FormItemContext.Provider>\n    );\n  },\n);\nFormItem.displayName = \"FormItem\";\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && \"text-destructive\", className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n});\nFormLabel.displayName = \"FormLabel\";\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } = useFormField();\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = \"FormControl\";\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn(\"text-[0.8rem] text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n});\nFormDescription.displayName = \"FormDescription\";\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn(\"text-[0.8rem] font-medium text-destructive\", className)}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n});\nFormMessage.displayName = \"FormMessage\";\n\nexport {\n  // eslint-disable-next-line react-refresh/only-export-components\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "ui/src/components/ui/input.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    )\n  }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n"
  },
  {
    "path": "ui/src/components/ui/label.tsx",
    "content": "import * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n)\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n"
  },
  {
    "path": "ui/src/components/ui/pagination.tsx",
    "content": "import * as React from \"react\"\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  DotsHorizontalIcon,\n} from \"@radix-ui/react-icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ButtonProps, buttonVariants } from \"@/components/ui/button\"\n\nconst Pagination = ({ className, ...props }: React.ComponentProps<\"nav\">) => (\n  <nav\n    role=\"navigation\"\n    aria-label=\"pagination\"\n    className={cn(\"mx-auto flex w-full justify-center\", className)}\n    {...props}\n  />\n)\nPagination.displayName = \"Pagination\"\n\nconst PaginationContent = React.forwardRef<\n  HTMLUListElement,\n  React.ComponentProps<\"ul\">\n>(({ className, ...props }, ref) => (\n  <ul\n    ref={ref}\n    className={cn(\"flex flex-row items-center gap-1\", className)}\n    {...props}\n  />\n))\nPaginationContent.displayName = \"PaginationContent\"\n\nconst PaginationItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentProps<\"li\">\n>(({ className, ...props }, ref) => (\n  <li ref={ref} className={cn(\"\", className)} {...props} />\n))\nPaginationItem.displayName = \"PaginationItem\"\n\ntype PaginationLinkProps = {\n  isActive?: boolean\n} & Pick<ButtonProps, \"size\"> &\n  React.ComponentProps<\"a\">\n\nconst PaginationLink = ({\n  className,\n  isActive,\n  size = \"icon\",\n  ...props\n}: PaginationLinkProps) => (\n  <a\n    aria-current={isActive ? \"page\" : undefined}\n    className={cn(\n      buttonVariants({\n        variant: isActive ? \"outline\" : \"ghost\",\n        size,\n      }),\n      className\n    )}\n    {...props}\n  />\n)\nPaginationLink.displayName = \"PaginationLink\"\n\nconst PaginationPrevious = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to previous page\"\n    size=\"default\"\n    className={cn(\"gap-1 pl-2.5\", className)}\n    {...props}\n  >\n    <ChevronLeftIcon className=\"h-4 w-4\" />\n    <span>Previous</span>\n  </PaginationLink>\n)\nPaginationPrevious.displayName = \"PaginationPrevious\"\n\nconst PaginationNext = ({\n  className,\n  ...props\n}: React.ComponentProps<typeof PaginationLink>) => (\n  <PaginationLink\n    aria-label=\"Go to next page\"\n    size=\"default\"\n    className={cn(\"gap-1 pr-2.5\", className)}\n    {...props}\n  >\n    <span>Next</span>\n    <ChevronRightIcon className=\"h-4 w-4\" />\n  </PaginationLink>\n)\nPaginationNext.displayName = \"PaginationNext\"\n\nconst PaginationEllipsis = ({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) => (\n  <span\n    aria-hidden\n    className={cn(\"flex h-9 w-9 items-center justify-center\", className)}\n    {...props}\n  >\n    <DotsHorizontalIcon className=\"h-4 w-4\" />\n    <span className=\"sr-only\">More pages</span>\n  </span>\n)\nPaginationEllipsis.displayName = \"PaginationEllipsis\"\n\nexport {\n  Pagination,\n  PaginationContent,\n  PaginationLink,\n  PaginationItem,\n  PaginationPrevious,\n  PaginationNext,\n  PaginationEllipsis,\n}\n"
  },
  {
    "path": "ui/src/components/ui/popover.tsx",
    "content": "import * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Popover = PopoverPrimitive.Root\n\nconst PopoverTrigger = PopoverPrimitive.Trigger\n\nconst PopoverAnchor = PopoverPrimitive.Anchor\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n))\nPopoverContent.displayName = PopoverPrimitive.Content.displayName\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }\n"
  },
  {
    "path": "ui/src/components/ui/select.tsx",
    "content": "import * as React from \"react\"\nimport {\n  CaretSortIcon,\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronUpIcon,\n} from \"@radix-ui/react-icons\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <CaretSortIcon className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronUpIcon />\n  </SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronDownIcon />\n  </SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"px-2 py-1.5 text-sm font-semibold\", className)}\n    {...props}\n  />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute right-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <CheckIcon className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n}\n"
  },
  {
    "path": "ui/src/components/ui/separator.tsx",
    "content": "import * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n)\nSeparator.displayName = SeparatorPrimitive.Root.displayName\n\nexport { Separator }\n"
  },
  {
    "path": "ui/src/components/ui/table.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Table = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full overflow-auto\">\n    <table\n      ref={ref}\n      className={cn(\"w-full caption-bottom text-sm\", className)}\n      {...props}\n    />\n  </div>\n));\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn(\"[&_tr]:border-b\", className)} {...props} />\n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn(\"[&_tr:last-child]:border-0\", className)}\n    {...props}\n  />\n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      \"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n      className\n    )}\n    {...props}\n  />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      \"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n      className\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      \"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n      className\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\n      \"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n      className\n    )}\n    {...props}\n  />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn(\"mt-4 text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nTableCaption.displayName = \"TableCaption\";\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "ui/src/components/ui/tabs.tsx",
    "content": "import * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Tabs = TabsPrimitive.Root\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsList.displayName = TabsPrimitive.List.displayName\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
  },
  {
    "path": "ui/src/components/ui/toast.tsx",
    "content": "import * as React from \"react\"\nimport { Cross2Icon } from \"@radix-ui/react-icons\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst ToastProvider = ToastPrimitives.Provider\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className\n    )}\n    {...props}\n  />\n))\nToastViewport.displayName = ToastPrimitives.Viewport.displayName\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  )\n})\nToast.displayName = ToastPrimitives.Root.displayName\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className\n    )}\n    {...props}\n  />\n))\nToastAction.displayName = ToastPrimitives.Action.displayName\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <Cross2Icon className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n))\nToastClose.displayName = ToastPrimitives.Close.displayName\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold [&+div]:text-xs\", className)}\n    {...props}\n  />\n))\nToastTitle.displayName = ToastPrimitives.Title.displayName\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90\", className)}\n    {...props}\n  />\n))\nToastDescription.displayName = ToastPrimitives.Description.displayName\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n}\n"
  },
  {
    "path": "ui/src/components/ui/toaster.tsx",
    "content": "import { useToast } from \"@/hooks/use-toast\"\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from \"@/components/ui/toast\"\n\nexport function Toaster() {\n  const { toasts } = useToast()\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        )\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  )\n}\n"
  },
  {
    "path": "ui/src/containers/session-container.tsx",
    "content": "import SessionConsole from \"@/components/sessions/session-console\";\nimport { SessionViewer } from \"@/components/sessions/session-viewer\";\nimport { Button } from \"@/components/ui/button\";\nimport { useSessionsContext } from \"@/hooks/use-sessions-context\";\nimport { ArrowLeftIcon, ArrowRightIcon } from \"@radix-ui/react-icons\";\nimport { useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\n\nexport function SessionContainer() {\n  const { id } = useParams();\n\n  const { useSession } = useSessionsContext();\n  const { data: session, isLoading, isError } = useSession(id!);\n  const [showConsole, setShowConsole] = useState(true);\n  if (isLoading) return <div>Loading...</div>;\n  if (isError || !session) return <div>Error</div>;\n\n  return (\n    <div className=\"flex flex-col overflow-hidden items-center justify-center h-full w-full p-4\">\n      <div className=\"flex flex-col overflow-hidden items-center justify-center h-full w-full rounded-md bg-[var(--gray-2)] p-4 pt-2 gap-3\">\n        <div className=\"flex items-center overflow-hidden justify-center h-full w-full gap-3\">\n          <div\n            className={`flex flex-col items-center justify-center h-full flex-1 border border-[var(--gray-6)] relative rounded-md ${\n              showConsole ? \"w-2/3\" : \"w-full\"\n            }`}\n          >\n            <Button\n              variant=\"secondary\"\n              onClick={() => setShowConsole(!showConsole)}\n              className=\"text-primary bg-[var(--gray-3)] ml-auto px-3 rounded-lg absolute top-2 right-2\"\n            >\n              {showConsole ? (\n                <ArrowRightIcon className=\"w-4 h-4\" />\n              ) : (\n                <ArrowLeftIcon className=\"w-4 h-4\" />\n              )}\n            </Button>\n            <SessionViewer id={id!} />\n          </div>\n          {showConsole && (\n            <div className=\"flex flex-col items-center overflow-hidden w-1/3 justify-center h-full text-primary gap-2\">\n              <div className=\"flex flex-col items-center overflow-hidden justify-center w-full h-full border border-[var(--gray-6)] rounded-md overflow-hidden\">\n                {session && <SessionConsole id={id!} />}\n              </div>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "ui/src/contexts/sessions-context/index.tsx",
    "content": "export { SessionsContext, SessionsProvider } from \"./sessions-context\";\n"
  },
  {
    "path": "ui/src/contexts/sessions-context/sessions-context.tsx",
    "content": "import { createContext, useState } from \"react\";\nimport {\n  SessionsContextType,\n  SessionsProviderProps,\n} from \"./sessions-context.types\";\nimport {\n  getSessions,\n  getSessionDetails,\n  GetSessionDetailsResponse,\n  releaseBrowserSession,\n  ReleaseBrowserSessionResponse,\n  ReleaseBrowserSessionsError,\n  SessionDetails,\n} from \"@/steel-client\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { queryClient } from \"@/lib/query-client\";\nimport { ErrorResponse } from \"@remix-run/router\";\n\n// eslint-disable-next-line react-refresh/only-export-components\nexport const SessionsContext = createContext<SessionsContextType | undefined>(\n  undefined,\n);\n\nexport function SessionsProvider({\n  children,\n}: SessionsProviderProps): JSX.Element {\n  const [currentSession, setCurrentSession] =\n    useState<GetSessionDetailsResponse | null>(null);\n\n  const useSession = (id: string) =>\n    useQuery<SessionDetails, ErrorResponse>({\n      queryKey: [\"session\", id],\n      queryFn: async () => {\n        if (!id) {\n          const { error, data } = await getSessions();\n          if (error || !data) {\n            throw error;\n          }\n          return data?.sessions[0];\n        }\n        const { error, data } = await getSessionDetails({\n          path: {\n            sessionId: id,\n          },\n        });\n        if (error || !data) {\n          throw error;\n        }\n        return data;\n      },\n      retry: false,\n      refetchInterval: 1000,\n      onSuccess: () => {\n        setCurrentSession(currentSession);\n      },\n    });\n\n  const useReleaseSessionMutation = () =>\n    useMutation<\n      ReleaseBrowserSessionResponse,\n      ReleaseBrowserSessionsError,\n      string\n    >({\n      //@ts-expect-error Mutation function is not defined\n      mutationFn: async (id: string) => {\n        const { error, data } = await releaseBrowserSession({\n          path: {\n            sessionId: id,\n          },\n        });\n        if (error) {\n          throw error;\n        }\n        queryClient.refetchQueries({ queryKey: [\"session\", id] });\n        queryClient.invalidateQueries({ queryKey: [\"sessionLogs\", id] });\n        return data;\n      },\n      onSuccess: () => {\n        queryClient.invalidateQueries({ queryKey: [\"sessions\"] });\n      },\n    });\n\n  const contextValue = {\n    currentSession,\n    useSession,\n    useReleaseSessionMutation,\n  };\n\n  return (\n    <SessionsContext.Provider value={contextValue}>\n      {children}\n    </SessionsContext.Provider>\n  );\n}\n"
  },
  {
    "path": "ui/src/contexts/sessions-context/sessions-context.types.ts",
    "content": "import {\n  GetSessionDetailsError,\n  GetSessionDetailsResponse,\n  ReleaseBrowserSessionResponse,\n  ReleaseBrowserSessionsError,\n} from \"@/steel-client\";\nimport { ReactNode } from \"react\";\nimport { UseMutationResult, UseQueryResult } from \"@tanstack/react-query\";\n\nexport type SessionsContextType = {\n  useReleaseSessionMutation: () => UseMutationResult<\n    ReleaseBrowserSessionResponse,\n    ReleaseBrowserSessionsError,\n    string,\n    unknown\n  >;\n  useSession: (\n    id: string\n  ) => UseQueryResult<GetSessionDetailsResponse | null, GetSessionDetailsError>;\n};\n\nexport type SessionsProviderProps = {\n  children: ReactNode;\n};\n"
  },
  {
    "path": "ui/src/env.ts",
    "content": "import { z } from \"zod\";\n\nconst envSchema = z.object({\n  // Default to paths that Nginx will proxy to API_URL in all modes\n  VITE_API_URL: z.string().default(\"/api\"),\n  VITE_WS_URL: z.string().default(\"/ws\"),\n});\n\nexport const env = envSchema.parse(import.meta.env);\n"
  },
  {
    "path": "ui/src/fonts/Geist/LICENSE.TXT",
    "content": "Geist Sans and Geist Mono Font\n(C) 2023 Vercel, made in collaboration with basement.studio\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is available with a FAQ at: http://scripts.sil.org/OFL and copied below\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded,\nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION AND CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "ui/src/fonts/GeistMono/LICENSE.TXT",
    "content": "Geist Sans and Geist Mono Font\n(C) 2023 Vercel, made in collaboration with basement.studio\n\nThis Font Software is licensed under the SIL Open Font License, Version 1.1.\nThis license is available with a FAQ at: http://scripts.sil.org/OFL and copied below\n\n-----------------------------------------------------------\nSIL OPEN FONT LICENSE Version 1.1 - 26 February 2007\n-----------------------------------------------------------\n\nPREAMBLE\nThe goals of the Open Font License (OFL) are to stimulate worldwide\ndevelopment of collaborative font projects, to support the font creation\nefforts of academic and linguistic communities, and to provide a free and\nopen framework in which fonts may be shared and improved in partnership\nwith others.\n\nThe OFL allows the licensed fonts to be used, studied, modified and\nredistributed freely as long as they are not sold by themselves. The\nfonts, including any derivative works, can be bundled, embedded,\nredistributed and/or sold with any software provided that any reserved\nnames are not used by derivative works. The fonts and derivatives,\nhowever, cannot be released under any other type of license. The\nrequirement for fonts to remain under this license does not apply\nto any document created using the fonts or their derivatives.\n\nDEFINITIONS\n\"Font Software\" refers to the set of files released by the Copyright\nHolder(s) under this license and clearly marked as such. This may\ninclude source files, build scripts and documentation.\n\n\"Reserved Font Name\" refers to any names specified as such after the\ncopyright statement(s).\n\n\"Original Version\" refers to the collection of Font Software components as\ndistributed by the Copyright Holder(s).\n\n\"Modified Version\" refers to any derivative made by adding to, deleting,\nor substituting -- in part or in whole -- any of the components of the\nOriginal Version, by changing formats or by porting the Font Software to a\nnew environment.\n\n\"Author\" refers to any designer, engineer, programmer, technical\nwriter or other person who contributed to the Font Software.\n\nPERMISSION AND CONDITIONS\nPermission is hereby granted, free of charge, to any person obtaining\na copy of the Font Software, to use, study, copy, merge, embed, modify,\nredistribute, and sell modified and unmodified copies of the Font\nSoftware, subject to the following conditions:\n\n1) Neither the Font Software nor any of its individual components,\nin Original or Modified Versions, may be sold by itself.\n\n2) Original or Modified Versions of the Font Software may be bundled,\nredistributed and/or sold with any software, provided that each copy\ncontains the above copyright notice and this license. These can be\nincluded either as stand-alone text files, human-readable headers or\nin the appropriate machine-readable metadata fields within text or\nbinary files as long as those fields can be easily viewed by the user.\n\n3) No Modified Version of the Font Software may use the Reserved Font\nName(s) unless explicit written permission is granted by the corresponding\nCopyright Holder. This restriction only applies to the primary font name as\npresented to the users.\n\n4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font\nSoftware shall not be used to promote, endorse or advertise any\nModified Version, except to acknowledge the contribution(s) of the\nCopyright Holder(s) and the Author(s) or with their explicit written\npermission.\n\n5) The Font Software, modified or unmodified, in part or in whole,\nmust be distributed entirely under this license, and must not be\ndistributed under any other license. The requirement for fonts to\nremain under this license does not apply to any document created\nusing the Font Software.\n\nTERMINATION\nThis license becomes null and void if any of the above conditions are\nnot met.\n\nDISCLAIMER\nTHE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT\nOF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE\nCOPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nINCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL\nDAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM\nOTHER DEALINGS IN THE FONT SOFTWARE.\n"
  },
  {
    "path": "ui/src/hooks/use-sessions-context.ts",
    "content": "import { SessionsContext } from \"@/contexts/sessions-context\";\nimport { SessionsContextType } from \"@/contexts/sessions-context/sessions-context.types\";\nimport { useContext } from \"react\";\n\nexport function useSessionsContext(): SessionsContextType {\n  const context = useContext(SessionsContext);\n  if (!context) {\n    throw new Error(\n      \"useSessionsContext must be used within an SessionsProvider\"\n    );\n  }\n  return context;\n}\n"
  },
  {
    "path": "ui/src/hooks/use-toast.ts",
    "content": "\"use client\"\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,\n  ToastProps,\n} from \"@/components/ui/toast\"\n\nconst TOAST_LIMIT = 1\nconst TOAST_REMOVE_DELAY = 1000000\n\ntype ToasterToast = ToastProps & {\n  id: string\n  title?: React.ReactNode\n  description?: React.ReactNode\n  action?: ToastActionElement\n}\n\nconst actionTypes = {\n  ADD_TOAST: \"ADD_TOAST\",\n  UPDATE_TOAST: \"UPDATE_TOAST\",\n  DISMISS_TOAST: \"DISMISS_TOAST\",\n  REMOVE_TOAST: \"REMOVE_TOAST\",\n} as const\n\nlet count = 0\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER\n  return count.toString()\n}\n\ntype ActionType = typeof actionTypes\n\ntype Action =\n  | {\n      type: ActionType[\"ADD_TOAST\"]\n      toast: ToasterToast\n    }\n  | {\n      type: ActionType[\"UPDATE_TOAST\"]\n      toast: Partial<ToasterToast>\n    }\n  | {\n      type: ActionType[\"DISMISS_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n  | {\n      type: ActionType[\"REMOVE_TOAST\"]\n      toastId?: ToasterToast[\"id\"]\n    }\n\ninterface State {\n  toasts: ToasterToast[]\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId)\n    dispatch({\n      type: \"REMOVE_TOAST\",\n      toastId: toastId,\n    })\n  }, TOAST_REMOVE_DELAY)\n\n  toastTimeouts.set(toastId, timeout)\n}\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case \"ADD_TOAST\":\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),\n      }\n\n    case \"UPDATE_TOAST\":\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        ),\n      }\n\n    case \"DISMISS_TOAST\": {\n      const { toastId } = action\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId)\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id)\n        })\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false,\n              }\n            : t\n        ),\n      }\n    }\n    case \"REMOVE_TOAST\":\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: [],\n        }\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId),\n      }\n  }\n}\n\nconst listeners: Array<(state: State) => void> = []\n\nlet memoryState: State = { toasts: [] }\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action)\n  listeners.forEach((listener) => {\n    listener(memoryState)\n  })\n}\n\ntype Toast = Omit<ToasterToast, \"id\">\n\nfunction toast({ ...props }: Toast) {\n  const id = genId()\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: \"UPDATE_TOAST\",\n      toast: { ...props, id },\n    })\n  const dismiss = () => dispatch({ type: \"DISMISS_TOAST\", toastId: id })\n\n  dispatch({\n    type: \"ADD_TOAST\",\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss()\n      },\n    },\n  })\n\n  return {\n    id: id,\n    dismiss,\n    update,\n  }\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState)\n\n  React.useEffect(() => {\n    listeners.push(setState)\n    return () => {\n      const index = listeners.indexOf(setState)\n      if (index > -1) {\n        listeners.splice(index, 1)\n      }\n    }\n  }, [state])\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: \"DISMISS_TOAST\", toastId }),\n  }\n}\n\nexport { useToast, toast }\n"
  },
  {
    "path": "ui/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* *,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\n* {\n  margin: 0;\n}\n\nbody {\n  line-height: 1.5;\n  -webkit-font-smoothing: antialiased;\n  background: var(--tokens-colors-page-background, #191719) !important;\n  font-family: \"Inter\";\n  height: 100vh;\n  width: 100vw;\n}\n\nimg,\npicture,\nvideo,\ncanvas,\nsvg {\n  display: block;\n  max-width: 100%;\n}\n\ninput,\nbutton,\ntextarea,\nselect {\n  font: inherit;\n}\n\np,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  overflow-wrap: break-word;\n}\n\n#root,\n#__next {\n  isolation: isolate;\n}\n\n.html {\n  background: var(--tokens-colors-page-background, #191719) !important;\n} */\n\n@font-face {\n  font-family: \"Geist\";\n  src: url(\"./fonts/Geist/Geist-Regular.otf\") format(\"opentype\");\n  font-weight: normal;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: \"Geist\";\n  src: url(\"./fonts/Geist/Geist-Medium.otf\") format(\"opentype\");\n  font-weight: 500;\n}\n\n@font-face {\n  font-family: \"Geist\";\n  src: url(\"./fonts/Geist/Geist-SemiBold.otf\") format(\"opentype\");\n  font-weight: 600;\n}\n\n@font-face {\n  font-family: \"Geist\";\n  src: url(\"./fonts/Geist/Geist-Bold.otf\") format(\"opentype\");\n  font-weight: bold;\n}\n\n@font-face {\n  font-family: \"Geist\";\n  src: url(\"./fonts/Geist/Geist-Black.otf\") format(\"opentype\");\n  font-weight: 900;\n}\n\n@font-face {\n  font-family: \"Geist\";\n  src: url(\"./fonts/Geist/Geist-UltraLight.otf\") format(\"opentype\");\n  font-weight: 200;\n}\n\n@font-face {\n  font-family: \"Geist\";\n  src: url(\"./fonts/Geist/Geist-UltraBlack.otf\") format(\"opentype\");\n  font-weight: 950;\n}\n\n@font-face {\n  font-family: \"Geist\";\n  src: url(\"./fonts/Geist/Geist-Thin.otf\") format(\"opentype\");\n  font-weight: 100;\n}\n\n@font-face {\n  font-family: \"Geist Mono\";\n  src: url(\"./fonts/GeistMono/GeistMono-Regular.otf\") format(\"opentype\");\n  font-weight: normal;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: \"Geist Mono\";\n  src: url(\"./fonts/GeistMono/GeistMono-Medium.otf\") format(\"opentype\");\n  font-weight: 500;\n}\n\n@font-face {\n  font-family: \"Geist Mono\";\n  src: url(\"./fonts/GeistMono/GeistMono-SemiBold.otf\") format(\"opentype\");\n  font-weight: 600;\n}\n\n@font-face {\n  font-family: \"Geist Mono\";\n  src: url(\"./fonts/GeistMono/GeistMono-Bold.otf\") format(\"opentype\");\n  font-weight: bold;\n}\n\n@font-face {\n  font-family: \"Geist Mono\";\n  src: url(\"./fonts/GeistMono/GeistMono-Black.otf\") format(\"opentype\");\n  font-weight: 900;\n}\n\n@font-face {\n  font-family: \"Geist Mono\";\n  src: url(\"./fonts/GeistMono/GeistMono-UltraLight.otf\") format(\"opentype\");\n  font-weight: 200;\n}\n\n@font-face {\n  font-family: \"Geist Mono\";\n  src: url(\"./fonts/GeistMono/GeistMono-UltraBlack.otf\") format(\"opentype\");\n  font-weight: 950;\n}\n\n@font-face {\n  font-family: \"Geist Mono\";\n  src: url(\"./fonts/GeistMono/GeistMono-Thin.otf\") format(\"opentype\");\n  font-weight: 100;\n}\n\n#root {\n  height: 100vh;\n  width: 100vw;\n}\n\n@layer base {\n  html {\n    font-family: \"Geist\", sans-serif;\n  }\n  :root {\n    --base-1: 0 0% 98.82352941176471%;\n    --base-2: 0 0% 97.6470588235294%;\n    --base-3: 0 0% 94.11764705882352%;\n    --base-4: 0 0% 90.98039215686275%;\n    --base-5: 0 0% 87.84313725490196%;\n    --base-6: 0 0% 85.09803921568627%;\n    --base-7: 0 0% 80.7843137254902%;\n    --base-8: 0 0% 73.33333333333333%;\n    --base-9: 0 0% 55.294117647058826%;\n    --base-10: 0 0% 51.37254901960784%;\n    --base-11: 0 0% 39.21568627450981%;\n    --base-12: 0 0% 12.549019607843137%;\n    --accent-1: 240 33.33333333333333% 99.41176470588235%;\n    --accent-2: 225.00000000000006 100% 98.43137254901961%;\n    --accent-3: 222.35294117647067 89.47368421052634% 96.27450980392157%;\n    --accent-4: 224 100% 94.11764705882352%;\n    --accent-5: 223.99999999999997 100% 91.17647058823529%;\n    --accent-6: 225.48387096774192 100% 87.84313725490196%;\n    --accent-7: 226.15384615384616 86.66666666666669% 82.35294117647058%;\n    --accent-8: 225.9183673469388 75.38461538461539% 74.50980392156863%;\n    --accent-9: 226.0377358490566 70.04405286343612% 55.490196078431374%;\n    --accent-10: 226.2111801242236 65.18218623481782% 51.5686274509804%;\n    --accent-11: 225.9574468085106 55.73122529644269% 50.3921568627451%;\n    --accent-12: 226.2295081967213 49.59349593495935% 24.11764705882353%;\n    --background: 0 0% 98.82352941176471%;\n    --foreground: 0 0% 12.549019607843137%;\n    --card: 0 0% 97.6470588235294%;\n    --card-foreground: 0 0% 12.549019607843137%;\n    --popover: 0 0% 98.82352941176471%;\n    --popover-foreground: 0 0% 12.549019607843137%;\n    --primary: 0 0% 0%;\n    --primary-foreground: 0 0% 100%;\n    --secondary: 0 0% 97.6470588235294%;\n    --secondary-foreground: 0 0% 39.21568627450981%;\n    --muted: 0 0% 97.6470588235294%;\n    --muted-foreground: 0 0% 39.21568627450981%;\n    --accent: 226.2111801242236 65.18218623481782% 51.5686274509804%;\n    --accent-foreground: 0 0% 100%;\n    --destructive: 10.10869565217391 73.01587301587303% 50.588235294117645%;\n    --destructive-foreground: 7.868852459016397 49.59349593495935%\n      24.11764705882353%;\n    --border: 0 0% 85.09803921568627%;\n    --input: 0 0% 85.09803921568627%;\n    --ring: 0 0% 85.09803921568627%;\n    --radius: 0.5rem;\n  }\n  .dark {\n    --base-1: 0 0% 6.666666666666667%;\n    --base-2: 0 0% 9.803921568627452%;\n    --base-3: 0 0% 13.333333333333334%;\n    --base-4: 0 0% 16.470588235294116%;\n    --base-5: 0 0% 19.215686274509807%;\n    --base-6: 0 0% 22.745098039215687%;\n    --base-7: 0 0% 28.235294117647058%;\n    --base-8: 0 0% 37.64705882352941%;\n    --base-9: 0 0% 43.13725490196079%;\n    --base-10: 0 0% 48.23529411764706%;\n    --base-11: 0 0% 70.58823529411765%;\n    --base-12: 0 0% 93.33333333333333%;\n    --accent-1: 231.42857142857144 29.166666666666668% 9.411764705882353%;\n    --accent-2: 230 31.03448275862069% 11.372549019607844%;\n    --accent-3: 225.3061224489796 50.51546391752577% 19.019607843137255%;\n    --accent-4: 225.21739130434784 54.330708661417326% 24.901960784313726%;\n    --accent-5: 224.81012658227849 51.633986928104584% 30%;\n    --accent-6: 226.42857142857142 46.66666666666667% 35.294117647058826%;\n    --accent-7: 226.45161290322582 44.4976076555024% 40.98039215686274%;\n    --accent-8: 225.8181818181818 45.08196721311475% 47.843137254901954%;\n    --accent-9: 226.0377358490566 70.04405286343612% 55.490196078431374%;\n    --accent-10: 227.5 72.72727272727272% 61.1764705882353%;\n    --accent-11: 228.2474226804124 100% 80.98039215686275%;\n    --accent-12: 223.90243902439025 100% 91.9607843137255%;\n    --background: 0 0% 6.666666666666667%;\n    --foreground: 0 0% 93.33333333333333%;\n    --card: 0 0% 9.803921568627452%;\n    --card-foreground: 0 0% 93.33333333333333%;\n    --popover: 0 0% 6.666666666666667%;\n    --popover-foreground: 0 0% 93.33333333333333%;\n    --primary: 0 0% 100%;\n    --primary-foreground: 0 0% 0%;\n    --secondary: 0 0% 9.803921568627452%;\n    --secondary-foreground: 0 0% 70.58823529411765%;\n    --muted: 0 0% 9.803921568627452%;\n    --muted-foreground: 0 0% 70.58823529411765%;\n    --accent: 227.5 72.72727272727272% 61.1764705882353%;\n    --accent-foreground: 0 0% 100%;\n    --destructive: 10.94117647058823 81.73076923076924% 59.21568627450981%;\n    --destructive-foreground: 10 85.71428571428571% 89.01960784313725%;\n    --border: 0 0% 22.745098039215687%;\n    --input: 0 0% 22.745098039215687%;\n    --ring: 0 0% 22.745098039215687%;\n    --radius: 0.5rem;\n  }\n}\n"
  },
  {
    "path": "ui/src/lib/query-client.ts",
    "content": "import { QueryClient } from \"@tanstack/react-query\";\n\nexport const queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      staleTime: 1000 * 60 * 5, // 5 minutes\n    },\n  },\n});\n"
  },
  {
    "path": "ui/src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "ui/src/main.tsx",
    "content": "// import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport App from \"./App\";\nimport \"./index.css\";\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  // ENABLE STRICT MODE FOR DEV\n  // <React.StrictMode>\n  <App />\n  // </React.StrictMode>\n);\n"
  },
  {
    "path": "ui/src/root-layout.tsx",
    "content": "import { ThemeProvider } from \"@/components/theme-provider\";\nimport { QueryClientProvider } from \"@tanstack/react-query\";\nimport { SessionsProvider } from \"./contexts/sessions-context\";\nimport { queryClient } from \"./lib/query-client\";\nimport { SessionContainer } from \"./containers/session-container\";\nimport { Header } from \"@/components/header\";\nimport { Toaster } from \"@/components/ui/toaster\";\n\nexport default function RootLayout() {\n  return (\n    <QueryClientProvider client={queryClient}>\n      <SessionsProvider>\n        <ThemeProvider defaultTheme=\"dark\" storageKey=\"steel-ui-theme\">\n          <div className=\"flex flex-col h-screen overflow-hidden max-h-screen items-center justify-center flex-1 bg-secondary text-primary-foreground\">\n            <Header />\n            <div className=\"flex flex-col overflow-hidden flex-1 w-full\">\n              <SessionContainer />\n            </div>\n          </div>\n          <Toaster />\n        </ThemeProvider>\n      </SessionsProvider>\n    </QueryClientProvider>\n  );\n}\n"
  },
  {
    "path": "ui/src/steel-client/index.ts",
    "content": "// This file is auto-generated by @hey-api/openapi-ts\nexport * from \"./schemas.gen\";\nexport * from \"./services.gen\";\nexport * from \"./types.gen\";\n"
  },
  {
    "path": "ui/src/steel-client/schemas.gen.ts",
    "content": "// This file is auto-generated by @hey-api/openapi-ts\n\nexport const ScrapeRequestSchema = {\n  title: \"ScrapeRequest\",\n  type: \"object\",\n  properties: {\n    url: {\n      type: \"string\",\n    },\n    format: {\n      type: \"array\",\n      items: {\n        type: \"string\",\n        enum: [\"html\", \"readability\", \"cleaned_html\", \"markdown\"],\n      },\n    },\n    screenshot: {\n      type: \"boolean\",\n    },\n    pdf: {\n      type: \"boolean\",\n    },\n    proxyUrl: {\n      type: \"string\",\n      nullable: true,\n      description:\n        \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\",\n    },\n    delay: {\n      type: \"number\",\n    },\n    logUrl: {\n      type: \"string\",\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const ScrapeResponseSchema = {\n  title: \"ScrapeResponse\",\n  type: \"object\",\n  properties: {\n    content: {\n      type: \"object\",\n      additionalProperties: {},\n    },\n    metadata: {\n      type: \"object\",\n      properties: {\n        title: {\n          type: \"string\",\n        },\n        language: {\n          type: \"string\",\n        },\n        urlSource: {\n          type: \"string\",\n        },\n        timestamp: {\n          type: \"string\",\n          format: \"date-time\",\n        },\n        description: {\n          type: \"string\",\n        },\n        keywords: {\n          type: \"string\",\n        },\n        author: {\n          type: \"string\",\n        },\n        ogTitle: {\n          type: \"string\",\n        },\n        ogDescription: {\n          type: \"string\",\n        },\n        ogImage: {\n          type: \"string\",\n        },\n        ogUrl: {\n          type: \"string\",\n        },\n        ogSiteName: {\n          type: \"string\",\n        },\n        articleAuthor: {\n          type: \"string\",\n        },\n        publishedTime: {\n          type: \"string\",\n        },\n        modifiedTime: {\n          type: \"string\",\n        },\n        canonical: {\n          type: \"string\",\n        },\n        favicon: {\n          type: \"string\",\n        },\n        jsonLd: {},\n        statusCode: {\n          type: \"integer\",\n        },\n      },\n      required: [\"statusCode\"],\n      additionalProperties: false,\n    },\n    links: {\n      type: \"array\",\n      items: {\n        type: \"object\",\n        properties: {\n          url: {\n            type: \"string\",\n          },\n          text: {\n            type: \"string\",\n          },\n        },\n        required: [\"url\", \"text\"],\n        additionalProperties: false,\n      },\n    },\n    screenshot: {\n      type: \"string\",\n    },\n    pdf: {\n      type: \"string\",\n    },\n  },\n  required: [\"content\", \"metadata\", \"links\"],\n  additionalProperties: false,\n} as const;\n\nexport const ScreenshotRequestSchema = {\n  title: \"ScreenshotRequest\",\n  type: \"object\",\n  properties: {\n    url: {\n      type: \"string\",\n    },\n    proxyUrl: {\n      type: \"string\",\n      nullable: true,\n      description:\n        \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\",\n    },\n    delay: {\n      type: \"number\",\n    },\n    fullPage: {\n      type: \"boolean\",\n    },\n    logUrl: {\n      type: \"string\",\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const ScreenshotResponseSchema = {\n  title: \"ScreenshotResponse\",\n} as const;\n\nexport const PDFRequestSchema = {\n  title: \"PDFRequest\",\n  type: \"object\",\n  properties: {\n    url: {\n      type: \"string\",\n    },\n    proxyUrl: {\n      type: \"string\",\n      nullable: true,\n      description:\n        \"Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\",\n    },\n    delay: {\n      type: \"number\",\n    },\n    logUrl: {\n      type: \"string\",\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const PDFResponseSchema = {\n  title: \"PDFResponse\",\n} as const;\n\nexport const CreateSessionSchema = {\n  title: \"CreateSession\",\n  type: \"object\",\n  properties: {\n    sessionId: {\n      type: \"string\",\n      format: \"uuid\",\n      description: \"Unique identifier for the session\",\n    },\n    proxyUrl: {\n      type: \"string\",\n      description: \"Proxy URL to use for the session\",\n    },\n    userAgent: {\n      type: \"string\",\n      description: \"User agent string to use for the session\",\n    },\n    sessionContext: {\n      type: \"object\",\n      properties: {\n        cookies: {\n          type: \"array\",\n          items: {\n            type: \"object\",\n            properties: {\n              name: {\n                type: \"string\",\n                description: \"The name of the cookie\",\n              },\n              value: {\n                type: \"string\",\n                description: \"The value of the cookie\",\n              },\n              url: {\n                type: \"string\",\n                description: \"The URL of the cookie\",\n              },\n              domain: {\n                type: \"string\",\n                description: \"The domain of the cookie\",\n              },\n              path: {\n                type: \"string\",\n                description: \"The path of the cookie\",\n              },\n              secure: {\n                type: \"boolean\",\n                description: \"Whether the cookie is secure\",\n              },\n              httpOnly: {\n                type: \"boolean\",\n                description: \"Whether the cookie is HTTP only\",\n              },\n              sameSite: {\n                type: \"string\",\n                enum: [\"Strict\", \"Lax\", \"None\"],\n                description: \"The same site attribute of the cookie\",\n              },\n              size: {\n                type: \"number\",\n                description: \"The size of the cookie\",\n              },\n              expires: {\n                type: \"number\",\n                description: \"The expiration date of the cookie\",\n              },\n              partitionKey: {\n                type: \"object\",\n                properties: {\n                  topLevelSite: {\n                    type: \"string\",\n                    description:\n                      \"The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie.\",\n                  },\n                  hasCrossSiteAncestor: {\n                    type: \"boolean\",\n                    description:\n                      \"Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.\",\n                  },\n                },\n                required: [\"topLevelSite\", \"hasCrossSiteAncestor\"],\n                additionalProperties: false,\n                description: \"The partition key of the cookie\",\n              },\n              session: {\n                type: \"boolean\",\n                description: \"Whether the cookie is a session cookie\",\n              },\n              priority: {\n                type: \"string\",\n                enum: [\"Low\", \"Medium\", \"High\"],\n                description: \"The priority of the cookie\",\n              },\n              sameParty: {\n                type: \"boolean\",\n                description: \"Whether the cookie is a same party cookie\",\n              },\n              sourceScheme: {\n                type: \"string\",\n                enum: [\"Unset\", \"NonSecure\", \"Secure\"],\n                description: \"The source scheme of the cookie\",\n              },\n              sourcePort: {\n                type: \"number\",\n                description: \"The source port of the cookie\",\n              },\n            },\n            required: [\"name\", \"value\"],\n            additionalProperties: false,\n          },\n          description: \"Cookies to initialize in the session\",\n        },\n        localStorage: {\n          type: \"object\",\n          additionalProperties: {\n            type: \"object\",\n            additionalProperties: {\n              type: \"string\",\n            },\n          },\n          description:\n            \"Domain-specific localStorage items to initialize in the session\",\n        },\n        sessionStorage: {\n          type: \"object\",\n          additionalProperties: {\n            type: \"object\",\n            additionalProperties: {\n              type: \"string\",\n            },\n          },\n          description:\n            \"Domain-specific sessionStorage items to initialize in the session\",\n        },\n        indexedDB: {\n          type: \"object\",\n          additionalProperties: {\n            type: \"array\",\n            items: {\n              type: \"object\",\n              properties: {\n                id: {\n                  type: \"number\",\n                },\n                name: {\n                  type: \"string\",\n                },\n                data: {\n                  type: \"array\",\n                  items: {\n                    type: \"object\",\n                    properties: {\n                      id: {\n                        type: \"number\",\n                      },\n                      name: {\n                        type: \"string\",\n                      },\n                      records: {\n                        type: \"array\",\n                        items: {\n                          type: \"object\",\n                          properties: {\n                            key: {},\n                            value: {},\n                            blobFiles: {\n                              type: \"array\",\n                              items: {\n                                type: \"object\",\n                                properties: {\n                                  blobNumber: {\n                                    type: \"number\",\n                                  },\n                                  mimeType: {\n                                    type: \"string\",\n                                  },\n                                  size: {\n                                    type: \"number\",\n                                  },\n                                  filename: {\n                                    type: \"string\",\n                                  },\n                                  lastModified: {\n                                    type: \"string\",\n                                    format: \"date-time\",\n                                  },\n                                  path: {\n                                    type: \"string\",\n                                  },\n                                },\n                                required: [\"blobNumber\", \"mimeType\", \"size\"],\n                                additionalProperties: false,\n                              },\n                            },\n                          },\n                          additionalProperties: false,\n                        },\n                      },\n                    },\n                    required: [\"id\", \"name\", \"records\"],\n                    additionalProperties: false,\n                  },\n                },\n              },\n              required: [\"id\", \"name\", \"data\"],\n              additionalProperties: false,\n            },\n          },\n          description:\n            \"Domain-specific indexedDB items to initialize in the session\",\n        },\n      },\n      additionalProperties: false,\n      description: \"Session context data to be used in the created session\",\n    },\n    isSelenium: {\n      type: \"boolean\",\n      description: \"Indicates if Selenium is used in the session\",\n    },\n    blockAds: {\n      type: \"boolean\",\n      description: \"Flag to indicate if ads should be blocked in the session\",\n    },\n    optimizeBandwidth: {\n      anyOf: [\n        {\n          type: \"boolean\",\n        },\n        {\n          type: \"object\",\n          properties: {\n            blockImages: {\n              type: \"boolean\",\n            },\n            blockMedia: {\n              type: \"boolean\",\n            },\n            blockStylesheets: {\n              type: \"boolean\",\n            },\n            blockHosts: {\n              type: \"array\",\n              items: {\n                type: \"string\",\n              },\n            },\n            blockUrlPatterns: {\n              type: \"array\",\n              items: {\n                type: \"string\",\n              },\n            },\n          },\n          additionalProperties: false,\n        },\n      ],\n      description:\n        \"Enable bandwidth optimizations. Passing true enables all flags (except hosts/patterns). Object allows granular control.\",\n    },\n    skipFingerprintInjection: {\n      type: \"boolean\",\n      description:\n        \"Flag to indicate if fingerprint injection should be skipped for this session.\",\n    },\n    deviceConfig: {\n      type: \"object\",\n      properties: {\n        device: {\n          type: \"string\",\n          enum: [\"desktop\", \"mobile\"],\n          default: \"desktop\",\n        },\n      },\n      additionalProperties: false,\n      description:\n        \"Device configuration for the session. Specify 'mobile' for mobile device fingerprints and configurations.\",\n    },\n    logSinkUrl: {\n      type: \"string\",\n      description: \"Deprecated: Log sink URL to use for the session\",\n    },\n    extensions: {\n      type: \"array\",\n      items: {\n        type: \"string\",\n      },\n      description: \"Extensions to use for the session\",\n    },\n    persist: {\n      type: \"boolean\",\n      description: \"Flag to indicate if session should be persisted\",\n    },\n    userDataDir: {\n      type: \"string\",\n      description: \"User data directory path to use for the session\",\n    },\n    timezone: {\n      type: \"string\",\n      description: \"Timezone to use for the session\",\n    },\n    dimensions: {\n      type: \"object\",\n      properties: {\n        width: {\n          type: \"number\",\n        },\n        height: {\n          type: \"number\",\n        },\n      },\n      required: [\"width\", \"height\"],\n      additionalProperties: false,\n      description: \"Dimensions to use for the session\",\n    },\n    userPreferences: {\n      type: \"object\",\n      additionalProperties: {},\n      description:\n        \"Chrome user preferences to customize browser behavior (e.g., font size, popup blocking, notification settings)\",\n    },\n    extra: {\n      type: \"object\",\n      additionalProperties: {},\n      description: \"Extra metadata to help initialize the session\",\n    },\n    credentials: {\n      type: \"object\",\n      properties: {\n        autoSubmit: {\n          anyOf: [\n            {\n              type: \"boolean\",\n            },\n            {\n              not: {},\n            },\n          ],\n        },\n        blurFields: {\n          anyOf: [\n            {\n              type: \"boolean\",\n            },\n            {\n              not: {},\n            },\n          ],\n        },\n        exactOrigin: {\n          anyOf: [\n            {\n              type: \"boolean\",\n            },\n            {\n              not: {},\n            },\n          ],\n        },\n      },\n      additionalProperties: false,\n      description: \"Configuration for session credentials\",\n    },\n    headless: {\n      type: \"boolean\",\n      description: \"Headless mode for the session\",\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const SessionDetailsSchema = {\n  title: \"SessionDetails\",\n  type: \"object\",\n  properties: {\n    id: {\n      type: \"string\",\n      format: \"uuid\",\n      description: \"Unique identifier for the session\",\n    },\n    createdAt: {\n      type: \"string\",\n      format: \"date-time\",\n      description: \"Timestamp when the session started\",\n    },\n    status: {\n      type: \"string\",\n      enum: [\"idle\", \"live\", \"released\", \"failed\"],\n      description: \"Status of the session\",\n    },\n    duration: {\n      type: \"integer\",\n      description: \"Duration of the session in milliseconds\",\n    },\n    eventCount: {\n      type: \"integer\",\n      description: \"Number of events processed in the session\",\n    },\n    dimensions: {\n      type: \"object\",\n      properties: {\n        width: {\n          type: \"number\",\n        },\n        height: {\n          type: \"number\",\n        },\n      },\n      required: [\"width\", \"height\"],\n      additionalProperties: false,\n      description: \"Dimensions used for the session\",\n    },\n    timeout: {\n      type: \"integer\",\n      description: \"Session timeout duration in milliseconds\",\n    },\n    creditsUsed: {\n      type: \"integer\",\n      description: \"Amount of credits consumed by the session\",\n    },\n    websocketUrl: {\n      type: \"string\",\n      description: \"URL for the session's WebSocket connection\",\n    },\n    debugUrl: {\n      type: \"string\",\n      description:\n        \"URL for a viewing the live browser instance for the session\",\n    },\n    debuggerUrl: {\n      type: \"string\",\n      description: \"URL for debugging the session\",\n    },\n    sessionViewerUrl: {\n      type: \"string\",\n      description: \"URL to view session details\",\n    },\n    userAgent: {\n      type: \"string\",\n      description: \"User agent string used in the session\",\n    },\n    proxy: {\n      type: \"string\",\n      description: \"Proxy server used for the session\",\n    },\n    proxyTxBytes: {\n      type: \"integer\",\n      minimum: 0,\n      description: \"Amount of data transmitted through the proxy\",\n    },\n    proxyRxBytes: {\n      type: \"integer\",\n      minimum: 0,\n      description: \"Amount of data received through the proxy\",\n    },\n    solveCaptcha: {\n      type: \"boolean\",\n      description: \"Indicates if captcha solving is enabled\",\n    },\n    isSelenium: {\n      type: \"boolean\",\n      description: \"Indicates if Selenium is used in the session\",\n    },\n  },\n  required: [\n    \"id\",\n    \"createdAt\",\n    \"status\",\n    \"duration\",\n    \"eventCount\",\n    \"timeout\",\n    \"creditsUsed\",\n    \"websocketUrl\",\n    \"debugUrl\",\n    \"debuggerUrl\",\n    \"sessionViewerUrl\",\n    \"proxyTxBytes\",\n    \"proxyRxBytes\",\n  ],\n  additionalProperties: false,\n} as const;\n\nexport const MultipleSessionsSchema = {\n  title: \"MultipleSessions\",\n  type: \"object\",\n  properties: {\n    sessions: {\n      type: \"array\",\n      items: {\n        type: \"object\",\n        properties: {\n          id: {\n            type: \"string\",\n            format: \"uuid\",\n            description: \"Unique identifier for the session\",\n          },\n          createdAt: {\n            type: \"string\",\n            format: \"date-time\",\n            description: \"Timestamp when the session started\",\n          },\n          status: {\n            type: \"string\",\n            enum: [\"idle\", \"live\", \"released\", \"failed\"],\n            description: \"Status of the session\",\n          },\n          duration: {\n            type: \"integer\",\n            description: \"Duration of the session in milliseconds\",\n          },\n          eventCount: {\n            type: \"integer\",\n            description: \"Number of events processed in the session\",\n          },\n          dimensions: {\n            type: \"object\",\n            properties: {\n              width: {\n                type: \"number\",\n              },\n              height: {\n                type: \"number\",\n              },\n            },\n            required: [\"width\", \"height\"],\n            additionalProperties: false,\n            description: \"Dimensions used for the session\",\n          },\n          timeout: {\n            type: \"integer\",\n            description: \"Session timeout duration in milliseconds\",\n          },\n          creditsUsed: {\n            type: \"integer\",\n            description: \"Amount of credits consumed by the session\",\n          },\n          websocketUrl: {\n            type: \"string\",\n            description: \"URL for the session's WebSocket connection\",\n          },\n          debugUrl: {\n            type: \"string\",\n            description:\n              \"URL for a viewing the live browser instance for the session\",\n          },\n          debuggerUrl: {\n            type: \"string\",\n            description: \"URL for debugging the session\",\n          },\n          sessionViewerUrl: {\n            type: \"string\",\n            description: \"URL to view session details\",\n          },\n          userAgent: {\n            type: \"string\",\n            description: \"User agent string used in the session\",\n          },\n          proxy: {\n            type: \"string\",\n            description: \"Proxy server used for the session\",\n          },\n          proxyTxBytes: {\n            type: \"integer\",\n            minimum: 0,\n            description: \"Amount of data transmitted through the proxy\",\n          },\n          proxyRxBytes: {\n            type: \"integer\",\n            minimum: 0,\n            description: \"Amount of data received through the proxy\",\n          },\n          solveCaptcha: {\n            type: \"boolean\",\n            description: \"Indicates if captcha solving is enabled\",\n          },\n          isSelenium: {\n            type: \"boolean\",\n            description: \"Indicates if Selenium is used in the session\",\n          },\n        },\n        required: [\n          \"id\",\n          \"createdAt\",\n          \"status\",\n          \"duration\",\n          \"eventCount\",\n          \"timeout\",\n          \"creditsUsed\",\n          \"websocketUrl\",\n          \"debugUrl\",\n          \"debuggerUrl\",\n          \"sessionViewerUrl\",\n          \"proxyTxBytes\",\n          \"proxyRxBytes\",\n        ],\n        additionalProperties: false,\n      },\n    },\n  },\n  required: [\"sessions\"],\n  additionalProperties: false,\n} as const;\n\nexport const SessionContextSchemaSchema = {\n  title: \"SessionContextSchema\",\n  type: \"object\",\n  properties: {\n    cookies: {\n      type: \"array\",\n      items: {\n        type: \"object\",\n        properties: {\n          name: {\n            type: \"string\",\n            description: \"The name of the cookie\",\n          },\n          value: {\n            type: \"string\",\n            description: \"The value of the cookie\",\n          },\n          url: {\n            type: \"string\",\n            description: \"The URL of the cookie\",\n          },\n          domain: {\n            type: \"string\",\n            description: \"The domain of the cookie\",\n          },\n          path: {\n            type: \"string\",\n            description: \"The path of the cookie\",\n          },\n          secure: {\n            type: \"boolean\",\n            description: \"Whether the cookie is secure\",\n          },\n          httpOnly: {\n            type: \"boolean\",\n            description: \"Whether the cookie is HTTP only\",\n          },\n          sameSite: {\n            type: \"string\",\n            enum: [\"Strict\", \"Lax\", \"None\"],\n            description: \"The same site attribute of the cookie\",\n          },\n          size: {\n            type: \"number\",\n            description: \"The size of the cookie\",\n          },\n          expires: {\n            type: \"number\",\n            description: \"The expiration date of the cookie\",\n          },\n          partitionKey: {\n            type: \"object\",\n            properties: {\n              topLevelSite: {\n                type: \"string\",\n                description:\n                  \"The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie.\",\n              },\n              hasCrossSiteAncestor: {\n                type: \"boolean\",\n                description:\n                  \"Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.\",\n              },\n            },\n            required: [\"topLevelSite\", \"hasCrossSiteAncestor\"],\n            additionalProperties: false,\n            description: \"The partition key of the cookie\",\n          },\n          session: {\n            type: \"boolean\",\n            description: \"Whether the cookie is a session cookie\",\n          },\n          priority: {\n            type: \"string\",\n            enum: [\"Low\", \"Medium\", \"High\"],\n            description: \"The priority of the cookie\",\n          },\n          sameParty: {\n            type: \"boolean\",\n            description: \"Whether the cookie is a same party cookie\",\n          },\n          sourceScheme: {\n            type: \"string\",\n            enum: [\"Unset\", \"NonSecure\", \"Secure\"],\n            description: \"The source scheme of the cookie\",\n          },\n          sourcePort: {\n            type: \"number\",\n            description: \"The source port of the cookie\",\n          },\n        },\n        required: [\"name\", \"value\"],\n        additionalProperties: false,\n      },\n      description: \"Cookies to initialize in the session\",\n    },\n    localStorage: {\n      type: \"object\",\n      additionalProperties: {\n        type: \"object\",\n        additionalProperties: {\n          type: \"string\",\n        },\n      },\n      description:\n        \"Domain-specific localStorage items to initialize in the session\",\n    },\n    sessionStorage: {\n      type: \"object\",\n      additionalProperties: {\n        type: \"object\",\n        additionalProperties: {\n          type: \"string\",\n        },\n      },\n      description:\n        \"Domain-specific sessionStorage items to initialize in the session\",\n    },\n    indexedDB: {\n      type: \"object\",\n      additionalProperties: {\n        type: \"array\",\n        items: {\n          type: \"object\",\n          properties: {\n            id: {\n              type: \"number\",\n            },\n            name: {\n              type: \"string\",\n            },\n            data: {\n              type: \"array\",\n              items: {\n                type: \"object\",\n                properties: {\n                  id: {\n                    type: \"number\",\n                  },\n                  name: {\n                    type: \"string\",\n                  },\n                  records: {\n                    type: \"array\",\n                    items: {\n                      type: \"object\",\n                      properties: {\n                        key: {},\n                        value: {},\n                        blobFiles: {\n                          type: \"array\",\n                          items: {\n                            type: \"object\",\n                            properties: {\n                              blobNumber: {\n                                type: \"number\",\n                              },\n                              mimeType: {\n                                type: \"string\",\n                              },\n                              size: {\n                                type: \"number\",\n                              },\n                              filename: {\n                                type: \"string\",\n                              },\n                              lastModified: {\n                                type: \"string\",\n                                format: \"date-time\",\n                              },\n                              path: {\n                                type: \"string\",\n                              },\n                            },\n                            required: [\"blobNumber\", \"mimeType\", \"size\"],\n                            additionalProperties: false,\n                          },\n                        },\n                      },\n                      additionalProperties: false,\n                    },\n                  },\n                },\n                required: [\"id\", \"name\", \"records\"],\n                additionalProperties: false,\n              },\n            },\n          },\n          required: [\"id\", \"name\", \"data\"],\n          additionalProperties: false,\n        },\n      },\n      description:\n        \"Domain-specific indexedDB items to initialize in the session\",\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const RecordedEventsSchema = {\n  title: \"RecordedEvents\",\n  type: \"object\",\n  properties: {\n    events: {\n      type: \"array\",\n      description: \"Events to emit\",\n    },\n  },\n  required: [\"events\"],\n  additionalProperties: false,\n} as const;\n\nexport const ReleaseSessionSchema = {\n  title: \"ReleaseSession\",\n  type: \"object\",\n  properties: {\n    id: {\n      type: \"string\",\n      format: \"uuid\",\n      description: \"Unique identifier for the session\",\n    },\n    createdAt: {\n      type: \"string\",\n      format: \"date-time\",\n      description: \"Timestamp when the session started\",\n    },\n    status: {\n      type: \"string\",\n      enum: [\"idle\", \"live\", \"released\", \"failed\"],\n      description: \"Status of the session\",\n    },\n    duration: {\n      type: \"integer\",\n      description: \"Duration of the session in milliseconds\",\n    },\n    eventCount: {\n      type: \"integer\",\n      description: \"Number of events processed in the session\",\n    },\n    dimensions: {\n      type: \"object\",\n      properties: {\n        width: {\n          type: \"number\",\n        },\n        height: {\n          type: \"number\",\n        },\n      },\n      required: [\"width\", \"height\"],\n      additionalProperties: false,\n      description: \"Dimensions used for the session\",\n    },\n    timeout: {\n      type: \"integer\",\n      description: \"Session timeout duration in milliseconds\",\n    },\n    creditsUsed: {\n      type: \"integer\",\n      description: \"Amount of credits consumed by the session\",\n    },\n    websocketUrl: {\n      type: \"string\",\n      description: \"URL for the session's WebSocket connection\",\n    },\n    debugUrl: {\n      type: \"string\",\n      description:\n        \"URL for a viewing the live browser instance for the session\",\n    },\n    debuggerUrl: {\n      type: \"string\",\n      description: \"URL for debugging the session\",\n    },\n    sessionViewerUrl: {\n      type: \"string\",\n      description: \"URL to view session details\",\n    },\n    userAgent: {\n      type: \"string\",\n      description: \"User agent string used in the session\",\n    },\n    proxy: {\n      type: \"string\",\n      description: \"Proxy server used for the session\",\n    },\n    proxyTxBytes: {\n      type: \"integer\",\n      minimum: 0,\n      description: \"Amount of data transmitted through the proxy\",\n    },\n    proxyRxBytes: {\n      type: \"integer\",\n      minimum: 0,\n      description: \"Amount of data received through the proxy\",\n    },\n    solveCaptcha: {\n      type: \"boolean\",\n      description: \"Indicates if captcha solving is enabled\",\n    },\n    isSelenium: {\n      type: \"boolean\",\n      description: \"Indicates if Selenium is used in the session\",\n    },\n    success: {\n      type: \"boolean\",\n      description: \"Indicates if the session was successfully released\",\n    },\n  },\n  required: [\n    \"id\",\n    \"createdAt\",\n    \"status\",\n    \"duration\",\n    \"eventCount\",\n    \"timeout\",\n    \"creditsUsed\",\n    \"websocketUrl\",\n    \"debugUrl\",\n    \"debuggerUrl\",\n    \"sessionViewerUrl\",\n    \"proxyTxBytes\",\n    \"proxyRxBytes\",\n    \"success\",\n  ],\n  additionalProperties: false,\n} as const;\n\nexport const SessionStreamQuerySchema = {\n  title: \"SessionStreamQuery\",\n  type: \"object\",\n  properties: {\n    showControls: {\n      type: \"boolean\",\n      default: true,\n      description: \"Show controls in the browser iframe\",\n    },\n    theme: {\n      type: \"string\",\n      enum: [\"dark\", \"light\"],\n      default: \"dark\",\n      description: \"Theme of the browser iframe\",\n    },\n    interactive: {\n      type: \"boolean\",\n      default: true,\n      description: \"Make the browser iframe interactive\",\n    },\n    pageId: {\n      type: \"string\",\n      description: \"Page ID to connect to\",\n    },\n    pageIndex: {\n      type: \"string\",\n      description: \"Page index (or tab index) to connect to\",\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const SessionStreamResponseSchema = {\n  title: \"SessionStreamResponse\",\n  type: \"string\",\n  description: \"HTML content for the session streamer view\",\n} as const;\n\nexport const SessionLiveDetailsResponseSchema = {\n  title: \"SessionLiveDetailsResponse\",\n  type: \"object\",\n  properties: {\n    sessionViewerUrl: {\n      type: \"string\",\n    },\n    sessionViewerFullscreenUrl: {\n      type: \"string\",\n    },\n    websocketUrl: {\n      type: \"string\",\n    },\n    pages: {\n      type: \"array\",\n      items: {\n        type: \"object\",\n        properties: {\n          id: {\n            type: \"string\",\n          },\n          url: {\n            type: \"string\",\n          },\n          title: {\n            type: \"string\",\n          },\n          favicon: {\n            type: \"string\",\n            nullable: true,\n          },\n        },\n        required: [\"id\", \"url\", \"title\", \"favicon\"],\n        additionalProperties: false,\n      },\n    },\n    browserState: {\n      type: \"object\",\n      properties: {\n        status: {\n          type: \"string\",\n          enum: [\"idle\", \"live\", \"released\", \"failed\"],\n        },\n        userAgent: {\n          type: \"string\",\n        },\n        browserVersion: {\n          type: \"string\",\n        },\n        initialDimensions: {\n          type: \"object\",\n          properties: {\n            width: {\n              type: \"number\",\n            },\n            height: {\n              type: \"number\",\n            },\n          },\n          required: [\"width\", \"height\"],\n          additionalProperties: false,\n        },\n        pageCount: {\n          type: \"number\",\n        },\n      },\n      required: [\n        \"status\",\n        \"userAgent\",\n        \"browserVersion\",\n        \"initialDimensions\",\n        \"pageCount\",\n      ],\n      additionalProperties: false,\n    },\n  },\n  required: [\n    \"sessionViewerUrl\",\n    \"sessionViewerFullscreenUrl\",\n    \"websocketUrl\",\n    \"pages\",\n    \"browserState\",\n  ],\n  additionalProperties: false,\n} as const;\n\nexport const LogQuerySchemaSchema = {\n  title: \"LogQuerySchema\",\n  type: \"object\",\n  properties: {\n    startTime: {\n      type: \"string\",\n      format: \"date-time\",\n    },\n    endTime: {\n      type: \"string\",\n      format: \"date-time\",\n    },\n    eventTypes: {\n      type: \"string\",\n    },\n    pageId: {\n      type: \"string\",\n    },\n    targetType: {\n      type: \"string\",\n    },\n    limit: {\n      type: \"integer\",\n      minimum: 1,\n      maximum: 1000,\n      default: 100,\n    },\n    offset: {\n      type: \"integer\",\n      minimum: 0,\n      default: 0,\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const LogStatsSchemaSchema = {\n  title: \"LogStatsSchema\",\n  type: \"object\",\n  properties: {\n    totalEvents: {\n      type: \"number\",\n    },\n    oldestEvent: {\n      type: \"string\",\n      format: \"date-time\",\n      nullable: true,\n    },\n    newestEvent: {\n      type: \"string\",\n      format: \"date-time\",\n      nullable: true,\n    },\n    sizeBytes: {\n      type: \"number\",\n    },\n  },\n  required: [\"totalEvents\", \"oldestEvent\", \"newestEvent\", \"sizeBytes\"],\n  additionalProperties: false,\n} as const;\n\nexport const LogQueryResultSchemaSchema = {\n  title: \"LogQueryResultSchema\",\n  type: \"object\",\n  properties: {\n    events: {\n      type: \"array\",\n      items: {\n        type: \"object\",\n        additionalProperties: {},\n      },\n    },\n    total: {\n      type: \"number\",\n    },\n    hasMore: {\n      type: \"boolean\",\n    },\n  },\n  required: [\"events\", \"total\", \"hasMore\"],\n  additionalProperties: false,\n} as const;\n\nexport const ExportLogsSchemaSchema = {\n  title: \"ExportLogsSchema\",\n  type: \"object\",\n  properties: {\n    query: {\n      type: \"object\",\n      properties: {\n        startTime: {\n          type: \"string\",\n          format: \"date-time\",\n        },\n        endTime: {\n          type: \"string\",\n          format: \"date-time\",\n        },\n        eventTypes: {\n          type: \"string\",\n        },\n        pageId: {\n          type: \"string\",\n        },\n        targetType: {\n          type: \"string\",\n        },\n        limit: {\n          type: \"integer\",\n          minimum: 1,\n          maximum: 1000,\n          default: 100,\n        },\n        offset: {\n          type: \"integer\",\n          minimum: 0,\n          default: 0,\n        },\n      },\n      additionalProperties: false,\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const GetDevtoolsUrlSchemaSchema = {\n  title: \"GetDevtoolsUrlSchema\",\n  type: \"object\",\n  properties: {\n    pageId: {\n      type: \"string\",\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const LaunchRequestSchema = {\n  title: \"LaunchRequest\",\n  type: \"object\",\n  properties: {\n    options: {\n      type: \"object\",\n      properties: {\n        args: {\n          type: \"array\",\n          items: {\n            type: \"string\",\n          },\n        },\n        chromiumSandbox: {\n          type: \"boolean\",\n        },\n        devtools: {\n          type: \"boolean\",\n        },\n        downloadsPath: {\n          type: \"string\",\n        },\n        headless: {\n          type: \"boolean\",\n        },\n        ignoreDefaultArgs: {\n          anyOf: [\n            {\n              type: \"boolean\",\n            },\n            {\n              type: \"array\",\n              items: {\n                type: \"string\",\n              },\n            },\n          ],\n        },\n        proxyUrl: {\n          type: \"string\",\n        },\n        timeout: {\n          type: \"number\",\n        },\n        tracesDir: {\n          type: \"string\",\n        },\n      },\n      additionalProperties: false,\n    },\n    req: {},\n    stealth: {\n      type: \"boolean\",\n    },\n    cookies: {\n      type: \"array\",\n    },\n    userAgent: {\n      type: \"string\",\n    },\n    extensions: {\n      type: \"array\",\n      items: {\n        type: \"string\",\n      },\n    },\n    logSinkUrl: {\n      type: \"string\",\n      description: \"Deprecated\",\n    },\n    customHeaders: {\n      type: \"object\",\n      additionalProperties: {\n        type: \"string\",\n      },\n    },\n    timezone: {\n      type: \"string\",\n    },\n    dimensions: {\n      type: \"object\",\n      properties: {\n        width: {\n          type: \"number\",\n        },\n        height: {\n          type: \"number\",\n        },\n      },\n      required: [\"width\", \"height\"],\n      additionalProperties: false,\n      nullable: true,\n    },\n  },\n  required: [\"options\"],\n  additionalProperties: false,\n} as const;\n\nexport const LaunchResponseSchema = {\n  title: \"LaunchResponse\",\n  type: \"object\",\n  properties: {\n    success: {\n      type: \"boolean\",\n    },\n  },\n  required: [\"success\"],\n  additionalProperties: false,\n} as const;\n\nexport const FileUploadRequestSchema = {\n  title: \"FileUploadRequest\",\n  type: \"object\",\n  properties: {\n    file: {\n      description: \"The file to upload (binary) or URL string to download from\",\n    },\n    path: {\n      type: \"string\",\n      description: \"Path to the file in the storage system\",\n    },\n  },\n  additionalProperties: false,\n} as const;\n\nexport const FileDetailsSchema = {\n  title: \"FileDetails\",\n  type: \"object\",\n  properties: {\n    path: {\n      type: \"string\",\n      description: \"Path to the file in the storage system\",\n    },\n    size: {\n      type: \"number\",\n      description: \"Size of the file in bytes\",\n    },\n    lastModified: {\n      type: \"string\",\n      format: \"date-time\",\n      description: \"Timestamp when the file was last updated\",\n    },\n  },\n  required: [\"path\", \"size\", \"lastModified\"],\n  additionalProperties: false,\n} as const;\n\nexport const MultipleFilesSchema = {\n  title: \"MultipleFiles\",\n  type: \"object\",\n  properties: {\n    data: {\n      type: \"array\",\n      items: {\n        type: \"object\",\n        properties: {\n          path: {\n            type: \"string\",\n            description: \"Path to the file in the storage system\",\n          },\n          size: {\n            type: \"number\",\n            description: \"Size of the file in bytes\",\n          },\n          lastModified: {\n            type: \"string\",\n            format: \"date-time\",\n            description: \"Timestamp when the file was last updated\",\n          },\n        },\n        required: [\"path\", \"size\", \"lastModified\"],\n        additionalProperties: false,\n      },\n      description: \"Array of files for the current page\",\n    },\n  },\n  required: [\"data\"],\n  additionalProperties: false,\n} as const;\n"
  },
  {
    "path": "ui/src/steel-client/services.gen.ts",
    "content": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport {\n  createClient,\n  createConfig,\n  type Options,\n  formDataBodySerializer,\n} from \"@hey-api/client-fetch\";\nimport {\n  type ScrapeData,\n  type ScrapeError,\n  type ScrapeResponse2,\n  type ScreenshotData,\n  type ScreenshotError,\n  type ScreenshotResponse2,\n  type PdfData,\n  type PdfError,\n  type PdfResponse,\n  type HealthError,\n  type HealthResponse,\n  type LaunchBrowserSessionData,\n  type LaunchBrowserSessionError,\n  type LaunchBrowserSessionResponse,\n  type GetSessionsError,\n  type GetSessionsResponse,\n  type GetSessionDetailsData,\n  type GetSessionDetailsError,\n  type GetSessionDetailsResponse,\n  type GetBrowserContextData,\n  type GetBrowserContextError,\n  type GetBrowserContextResponse,\n  type ReleaseBrowserSessionData,\n  type ReleaseBrowserSessionError,\n  type ReleaseBrowserSessionResponse,\n  type ReleaseBrowserSessionsError,\n  type ReleaseBrowserSessionsResponse,\n  type GetSessionDebuggerStreamData,\n  type GetSessionDebuggerStreamError,\n  type GetSessionDebuggerStreamResponse,\n  type ReceiveEventsData,\n  type ReceiveEventsError,\n  type ReceiveEventsResponse,\n  type GetSessionLiveDetailsData,\n  type GetSessionLiveDetailsError,\n  type GetSessionLiveDetailsResponse,\n  type ScrapeSessionData,\n  type ScrapeSessionError,\n  type ScrapeSessionResponse,\n  type ScreenshotSessionData,\n  type ScreenshotSessionError,\n  type ScreenshotSessionResponse,\n  type PdfSessionData,\n  type PdfSessionError,\n  type PdfSessionResponse,\n  type GetDevtoolsUrlData,\n  type GetDevtoolsUrlError,\n  type GetDevtoolsUrlResponse,\n  type UploadFileData,\n  type UploadFileError,\n  type UploadFileResponse,\n  type ListFilesData,\n  type ListFilesError,\n  type ListFilesResponse,\n  type DeleteAllFilesData,\n  type DeleteAllFilesError,\n  type DeleteAllFilesResponse,\n  type DownloadFileData,\n  type DownloadFileError,\n  type DownloadFileResponse,\n  type DeleteFileData,\n  type DeleteFileError,\n  type DeleteFileResponse,\n  type DownloadArchiveData,\n  type DownloadArchiveError,\n  type DownloadArchiveResponse,\n  type GetV1LogsQueryData,\n  type GetV1LogsQueryError,\n  type GetV1LogsQueryResponse,\n  type GetV1LogsStatsError,\n  type GetV1LogsStatsResponse,\n  type GetV1LogsStreamError,\n  type GetV1LogsStreamResponse,\n  type PostV1LogsExportData,\n  type PostV1LogsExportError,\n  type PostV1LogsExportResponse,\n  type DeleteV1LogsError,\n  type DeleteV1LogsResponse,\n  ScrapeResponseTransformer,\n  LaunchBrowserSessionResponseTransformer,\n  GetSessionDetailsResponseTransformer,\n  ReleaseBrowserSessionResponseTransformer,\n  ReleaseBrowserSessionsResponseTransformer,\n  ScrapeSessionResponseTransformer,\n  UploadFileResponseTransformer,\n} from \"./types.gen\";\n\nexport const client = createClient(createConfig());\n\n/**\n * Scrape a URL\n * Scrape a URL\n */\nexport const scrape = <ThrowOnError extends boolean = false>(\n  options?: Options<ScrapeData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    ScrapeResponse2,\n    ScrapeError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/scrape\",\n    responseTransformer: ScrapeResponseTransformer,\n  });\n};\n\n/**\n * Take a screenshot\n * Take a screenshot\n */\nexport const screenshot = <ThrowOnError extends boolean = false>(\n  options?: Options<ScreenshotData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    ScreenshotResponse2,\n    ScreenshotError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/screenshot\",\n  });\n};\n\n/**\n * Get the PDF content of a page\n * Get the PDF content of a page\n */\nexport const pdf = <ThrowOnError extends boolean = false>(\n  options?: Options<PdfData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<PdfResponse, PdfError, ThrowOnError>({\n    ...options,\n    url: \"/v1/pdf\",\n  });\n};\n\n/**\n * Check if the server and browser are running\n * Check if the server and browser are running\n */\nexport const health = <ThrowOnError extends boolean = false>(\n  options?: Options<unknown, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    HealthResponse,\n    HealthError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/health\",\n  });\n};\n\n/**\n * Launch a browser session\n * Launch a browser session\n */\nexport const launchBrowserSession = <ThrowOnError extends boolean = false>(\n  options?: Options<LaunchBrowserSessionData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    LaunchBrowserSessionResponse,\n    LaunchBrowserSessionError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions\",\n    responseTransformer: LaunchBrowserSessionResponseTransformer,\n  });\n};\n\n/**\n * Get all sessions\n * Get all sessions\n */\nexport const getSessions = <ThrowOnError extends boolean = false>(\n  options?: Options<unknown, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetSessionsResponse,\n    GetSessionsError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions\",\n  });\n};\n\n/**\n * Get session details\n * Get session details\n */\nexport const getSessionDetails = <ThrowOnError extends boolean = false>(\n  options: Options<GetSessionDetailsData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetSessionDetailsResponse,\n    GetSessionDetailsError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{sessionId}\",\n    responseTransformer: GetSessionDetailsResponseTransformer,\n  });\n};\n\n/**\n * Get a browser context\n * Get a browser context\n */\nexport const getBrowserContext = <ThrowOnError extends boolean = false>(\n  options: Options<GetBrowserContextData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetBrowserContextResponse,\n    GetBrowserContextError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{sessionId}/context\",\n  });\n};\n\n/**\n * Release a browser session\n * Release a browser session\n */\nexport const releaseBrowserSession = <ThrowOnError extends boolean = false>(\n  options: Options<ReleaseBrowserSessionData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    ReleaseBrowserSessionResponse,\n    ReleaseBrowserSessionError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{sessionId}/release\",\n    responseTransformer: ReleaseBrowserSessionResponseTransformer,\n  });\n};\n\n/**\n * Release browser sessions\n * Release browser sessions\n */\nexport const releaseBrowserSessions = <ThrowOnError extends boolean = false>(\n  options?: Options<unknown, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    ReleaseBrowserSessionsResponse,\n    ReleaseBrowserSessionsError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/release\",\n    responseTransformer: ReleaseBrowserSessionsResponseTransformer,\n  });\n};\n\n/**\n * Get session debugger view\n * Returns an HTML page with a live debugger view of the session\n */\nexport const getSessionDebuggerStream = <ThrowOnError extends boolean = false>(\n  options?: Options<GetSessionDebuggerStreamData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetSessionDebuggerStreamResponse,\n    GetSessionDebuggerStreamError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/debug\",\n  });\n};\n\n/**\n * Receive recorded events from the browser\n * Receive recorded events from the browser\n */\nexport const receiveEvents = <ThrowOnError extends boolean = false>(\n  options?: Options<ReceiveEventsData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    ReceiveEventsResponse,\n    ReceiveEventsError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/events\",\n  });\n};\n\n/**\n * Get session live details\n * Returns the live state of the session, including pages, tabs, and browser state\n */\nexport const getSessionLiveDetails = <ThrowOnError extends boolean = false>(\n  options: Options<GetSessionLiveDetailsData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetSessionLiveDetailsResponse,\n    GetSessionLiveDetailsError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{id}/live-details\",\n  });\n};\n\n/**\n * Scrape Current Session\n * Scrape Current Session\n */\nexport const scrapeSession = <ThrowOnError extends boolean = false>(\n  options?: Options<ScrapeSessionData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    ScrapeSessionResponse,\n    ScrapeSessionError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/scrape\",\n    responseTransformer: ScrapeSessionResponseTransformer,\n  });\n};\n\n/**\n * Take Screenshot of Current Session\n * Take Screenshot of Current Session\n */\nexport const screenshotSession = <ThrowOnError extends boolean = false>(\n  options?: Options<ScreenshotSessionData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    ScreenshotSessionResponse,\n    ScreenshotSessionError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/screenshot\",\n  });\n};\n\n/**\n * Generate PDF of Current Session\n * Generate PDF of Current Session\n */\nexport const pdfSession = <ThrowOnError extends boolean = false>(\n  options?: Options<PdfSessionData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    PdfSessionResponse,\n    PdfSessionError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/pdf\",\n  });\n};\n\n/**\n * Get the URL for the DevTools inspector\n * Get the URL for the DevTools inspector\n */\nexport const getDevtoolsUrl = <ThrowOnError extends boolean = false>(\n  options?: Options<GetDevtoolsUrlData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetDevtoolsUrlResponse,\n    GetDevtoolsUrlError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/devtools/inspector.html\",\n  });\n};\n\n/**\n * Upload a file\n * Uploads a file to a session via `multipart/form-data` with a `file` field that accepts either binary data or a URL string to download from, and an optional `path` field for the file storage path.\n */\nexport const uploadFile = <ThrowOnError extends boolean = false>(\n  options: Options<UploadFileData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    UploadFileResponse,\n    UploadFileError,\n    ThrowOnError\n  >({\n    ...options,\n    ...formDataBodySerializer,\n    headers: {\n      \"Content-Type\": null,\n      ...options?.headers,\n    },\n    url: \"/v1/sessions/{sessionId}/files\",\n    responseTransformer: UploadFileResponseTransformer,\n  });\n};\n\n/**\n * List files\n * List all files from the session in descending order.\n */\nexport const listFiles = <ThrowOnError extends boolean = false>(\n  options: Options<ListFilesData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    ListFilesResponse,\n    ListFilesError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{sessionId}/files\",\n  });\n};\n\n/**\n * Delete all files\n * Delete all files from a session\n */\nexport const deleteAllFiles = <ThrowOnError extends boolean = false>(\n  options: Options<DeleteAllFilesData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).delete<\n    DeleteAllFilesResponse,\n    DeleteAllFilesError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{sessionId}/files\",\n  });\n};\n\n/**\n * Download a file\n * Download a file from a session\n */\nexport const downloadFile = <ThrowOnError extends boolean = false>(\n  options: Options<DownloadFileData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    DownloadFileResponse,\n    DownloadFileError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{sessionId}/files/{*}\",\n  });\n};\n\n/**\n * Delete a file\n * Delete a file from a session\n */\nexport const deleteFile = <ThrowOnError extends boolean = false>(\n  options: Options<DeleteFileData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).delete<\n    DeleteFileResponse,\n    DeleteFileError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{sessionId}/files/{*}\",\n  });\n};\n\n/**\n * Download archive\n * Download all files from the session as a zip archive.\n */\nexport const downloadArchive = <ThrowOnError extends boolean = false>(\n  options: Options<DownloadArchiveData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    DownloadArchiveResponse,\n    DownloadArchiveError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/sessions/{sessionId}/files.zip\",\n  });\n};\n\n/**\n * Query browser logs from local storage\n */\nexport const getV1LogsQuery = <ThrowOnError extends boolean = false>(\n  options?: Options<GetV1LogsQueryData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetV1LogsQueryResponse,\n    GetV1LogsQueryError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/logs/query\",\n  });\n};\n\n/**\n * Get statistics about stored browser logs\n */\nexport const getV1LogsStats = <ThrowOnError extends boolean = false>(\n  options?: Options<unknown, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetV1LogsStatsResponse,\n    GetV1LogsStatsError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/logs/stats\",\n  });\n};\n\n/**\n * Stream browser logs in real-time using SSE\n */\nexport const getV1LogsStream = <ThrowOnError extends boolean = false>(\n  options?: Options<unknown, ThrowOnError>,\n) => {\n  return (options?.client ?? client).get<\n    GetV1LogsStreamResponse,\n    GetV1LogsStreamError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/logs/stream\",\n  });\n};\n\n/**\n * Export browser logs to Parquet format\n */\nexport const postV1LogsExport = <ThrowOnError extends boolean = false>(\n  options?: Options<PostV1LogsExportData, ThrowOnError>,\n) => {\n  return (options?.client ?? client).post<\n    PostV1LogsExportResponse,\n    PostV1LogsExportError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/logs/export\",\n  });\n};\n\n/**\n * Clear all browser logs from storage\n */\nexport const deleteV1Logs = <ThrowOnError extends boolean = false>(\n  options?: Options<unknown, ThrowOnError>,\n) => {\n  return (options?.client ?? client).delete<\n    DeleteV1LogsResponse,\n    DeleteV1LogsError,\n    ThrowOnError\n  >({\n    ...options,\n    url: \"/v1/logs/\",\n  });\n};\n"
  },
  {
    "path": "ui/src/steel-client/types.gen.ts",
    "content": "// This file is auto-generated by @hey-api/openapi-ts\n\nexport type ScrapeRequest = {\n  url?: string;\n  format?: Array<\"html\" | \"readability\" | \"cleaned_html\" | \"markdown\">;\n  screenshot?: boolean;\n  pdf?: boolean;\n  /**\n   * Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\n   */\n  proxyUrl?: string | null;\n  delay?: number;\n  logUrl?: string;\n};\n\nexport type ScrapeResponse = {\n  content: {\n    [key: string]: unknown;\n  };\n  metadata: {\n    title?: string;\n    language?: string;\n    urlSource?: string;\n    timestamp?: Date;\n    description?: string;\n    keywords?: string;\n    author?: string;\n    ogTitle?: string;\n    ogDescription?: string;\n    ogImage?: string;\n    ogUrl?: string;\n    ogSiteName?: string;\n    articleAuthor?: string;\n    publishedTime?: string;\n    modifiedTime?: string;\n    canonical?: string;\n    favicon?: string;\n    jsonLd?: unknown;\n    statusCode: number;\n  };\n  links: Array<{\n    url: string;\n    text: string;\n  }>;\n  screenshot?: string;\n  pdf?: string;\n};\n\nexport type ScreenshotRequest = {\n  url?: string;\n  /**\n   * Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\n   */\n  proxyUrl?: string | null;\n  delay?: number;\n  fullPage?: boolean;\n  logUrl?: string;\n};\n\nexport type ScreenshotResponse = unknown;\n\nexport type PDFRequest = {\n  url?: string;\n  /**\n   * Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.\n   */\n  proxyUrl?: string | null;\n  delay?: number;\n  logUrl?: string;\n};\n\nexport type PDFResponse = unknown;\n\nexport type CreateSession = {\n  /**\n   * Unique identifier for the session\n   */\n  sessionId?: string;\n  /**\n   * Proxy URL to use for the session\n   */\n  proxyUrl?: string;\n  /**\n   * User agent string to use for the session\n   */\n  userAgent?: string;\n  /**\n   * Session context data to be used in the created session\n   */\n  sessionContext?: {\n    /**\n     * Cookies to initialize in the session\n     */\n    cookies?: Array<{\n      /**\n       * The name of the cookie\n       */\n      name: string;\n      /**\n       * The value of the cookie\n       */\n      value: string;\n      /**\n       * The URL of the cookie\n       */\n      url?: string;\n      /**\n       * The domain of the cookie\n       */\n      domain?: string;\n      /**\n       * The path of the cookie\n       */\n      path?: string;\n      /**\n       * Whether the cookie is secure\n       */\n      secure?: boolean;\n      /**\n       * Whether the cookie is HTTP only\n       */\n      httpOnly?: boolean;\n      /**\n       * The same site attribute of the cookie\n       */\n      sameSite?: \"Strict\" | \"Lax\" | \"None\";\n      /**\n       * The size of the cookie\n       */\n      size?: number;\n      /**\n       * The expiration date of the cookie\n       */\n      expires?: number;\n      /**\n       * The partition key of the cookie\n       */\n      partitionKey?: {\n        /**\n         * The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie.\n         */\n        topLevelSite: string;\n        /**\n         * Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.\n         */\n        hasCrossSiteAncestor: boolean;\n      };\n      /**\n       * Whether the cookie is a session cookie\n       */\n      session?: boolean;\n      /**\n       * The priority of the cookie\n       */\n      priority?: \"Low\" | \"Medium\" | \"High\";\n      /**\n       * Whether the cookie is a same party cookie\n       */\n      sameParty?: boolean;\n      /**\n       * The source scheme of the cookie\n       */\n      sourceScheme?: \"Unset\" | \"NonSecure\" | \"Secure\";\n      /**\n       * The source port of the cookie\n       */\n      sourcePort?: number;\n    }>;\n    /**\n     * Domain-specific localStorage items to initialize in the session\n     */\n    localStorage?: {\n      [key: string]: {\n        [key: string]: string;\n      };\n    };\n    /**\n     * Domain-specific sessionStorage items to initialize in the session\n     */\n    sessionStorage?: {\n      [key: string]: {\n        [key: string]: string;\n      };\n    };\n    /**\n     * Domain-specific indexedDB items to initialize in the session\n     */\n    indexedDB?: {\n      [key: string]: Array<{\n        id: number;\n        name: string;\n        data: Array<{\n          id: number;\n          name: string;\n          records: Array<{\n            key?: unknown;\n            value?: unknown;\n            blobFiles?: Array<{\n              blobNumber: number;\n              mimeType: string;\n              size: number;\n              filename?: string;\n              lastModified?: Date;\n              path?: string;\n            }>;\n          }>;\n        }>;\n      }>;\n    };\n  };\n  /**\n   * Indicates if Selenium is used in the session\n   */\n  isSelenium?: boolean;\n  /**\n   * Flag to indicate if ads should be blocked in the session\n   */\n  blockAds?: boolean;\n  /**\n   * Enable bandwidth optimizations. Passing true enables all flags (except hosts/patterns). Object allows granular control.\n   */\n  optimizeBandwidth?:\n    | boolean\n    | {\n        blockImages?: boolean;\n        blockMedia?: boolean;\n        blockStylesheets?: boolean;\n        blockHosts?: Array<string>;\n        blockUrlPatterns?: Array<string>;\n      };\n  /**\n   * Flag to indicate if fingerprint injection should be skipped for this session.\n   */\n  skipFingerprintInjection?: boolean;\n  /**\n   * Device configuration for the session. Specify 'mobile' for mobile device fingerprints and configurations.\n   */\n  deviceConfig?: {\n    device?: \"desktop\" | \"mobile\";\n  };\n  /**\n   * Deprecated: Log sink URL to use for the session\n   */\n  logSinkUrl?: string;\n  /**\n   * Extensions to use for the session\n   */\n  extensions?: Array<string>;\n  /**\n   * Flag to indicate if session should be persisted\n   */\n  persist?: boolean;\n  /**\n   * User data directory path to use for the session\n   */\n  userDataDir?: string;\n  /**\n   * Timezone to use for the session\n   */\n  timezone?: string;\n  /**\n   * Dimensions to use for the session\n   */\n  dimensions?: {\n    width: number;\n    height: number;\n  };\n  /**\n   * Chrome user preferences to customize browser behavior (e.g., font size, popup blocking, notification settings)\n   */\n  userPreferences?: {\n    [key: string]: unknown;\n  };\n  /**\n   * Extra metadata to help initialize the session\n   */\n  extra?: {\n    [key: string]: unknown;\n  };\n  /**\n   * Configuration for session credentials\n   */\n  credentials?: {\n    autoSubmit?: boolean | unknown;\n    blurFields?: boolean | unknown;\n    exactOrigin?: boolean | unknown;\n  };\n  /**\n   * Headless mode for the session\n   */\n  headless?: boolean;\n};\n\nexport type device = \"desktop\" | \"mobile\";\n\nexport const device = {\n  DESKTOP: \"desktop\",\n  MOBILE: \"mobile\",\n} as const;\n\nexport type SessionDetails = {\n  /**\n   * Unique identifier for the session\n   */\n  id: string;\n  /**\n   * Timestamp when the session started\n   */\n  createdAt: Date;\n  /**\n   * Status of the session\n   */\n  status: \"idle\" | \"live\" | \"released\" | \"failed\";\n  /**\n   * Duration of the session in milliseconds\n   */\n  duration: number;\n  /**\n   * Number of events processed in the session\n   */\n  eventCount: number;\n  /**\n   * Dimensions used for the session\n   */\n  dimensions?: {\n    width: number;\n    height: number;\n  };\n  /**\n   * Session timeout duration in milliseconds\n   */\n  timeout: number;\n  /**\n   * Amount of credits consumed by the session\n   */\n  creditsUsed: number;\n  /**\n   * URL for the session's WebSocket connection\n   */\n  websocketUrl: string;\n  /**\n   * URL for a viewing the live browser instance for the session\n   */\n  debugUrl: string;\n  /**\n   * URL for debugging the session\n   */\n  debuggerUrl: string;\n  /**\n   * URL to view session details\n   */\n  sessionViewerUrl: string;\n  /**\n   * User agent string used in the session\n   */\n  userAgent?: string;\n  /**\n   * Proxy server used for the session\n   */\n  proxy?: string;\n  /**\n   * Amount of data transmitted through the proxy\n   */\n  proxyTxBytes: number;\n  /**\n   * Amount of data received through the proxy\n   */\n  proxyRxBytes: number;\n  /**\n   * Indicates if captcha solving is enabled\n   */\n  solveCaptcha?: boolean;\n  /**\n   * Indicates if Selenium is used in the session\n   */\n  isSelenium?: boolean;\n};\n\n/**\n * Status of the session\n */\nexport type status = \"idle\" | \"live\" | \"released\" | \"failed\";\n\n/**\n * Status of the session\n */\nexport const status = {\n  IDLE: \"idle\",\n  LIVE: \"live\",\n  RELEASED: \"released\",\n  FAILED: \"failed\",\n} as const;\n\nexport type MultipleSessions = {\n  sessions: Array<{\n    /**\n     * Unique identifier for the session\n     */\n    id: string;\n    /**\n     * Timestamp when the session started\n     */\n    createdAt: Date;\n    /**\n     * Status of the session\n     */\n    status: \"idle\" | \"live\" | \"released\" | \"failed\";\n    /**\n     * Duration of the session in milliseconds\n     */\n    duration: number;\n    /**\n     * Number of events processed in the session\n     */\n    eventCount: number;\n    /**\n     * Dimensions used for the session\n     */\n    dimensions?: {\n      width: number;\n      height: number;\n    };\n    /**\n     * Session timeout duration in milliseconds\n     */\n    timeout: number;\n    /**\n     * Amount of credits consumed by the session\n     */\n    creditsUsed: number;\n    /**\n     * URL for the session's WebSocket connection\n     */\n    websocketUrl: string;\n    /**\n     * URL for a viewing the live browser instance for the session\n     */\n    debugUrl: string;\n    /**\n     * URL for debugging the session\n     */\n    debuggerUrl: string;\n    /**\n     * URL to view session details\n     */\n    sessionViewerUrl: string;\n    /**\n     * User agent string used in the session\n     */\n    userAgent?: string;\n    /**\n     * Proxy server used for the session\n     */\n    proxy?: string;\n    /**\n     * Amount of data transmitted through the proxy\n     */\n    proxyTxBytes: number;\n    /**\n     * Amount of data received through the proxy\n     */\n    proxyRxBytes: number;\n    /**\n     * Indicates if captcha solving is enabled\n     */\n    solveCaptcha?: boolean;\n    /**\n     * Indicates if Selenium is used in the session\n     */\n    isSelenium?: boolean;\n  }>;\n};\n\nexport type SessionContextSchema = {\n  /**\n   * Cookies to initialize in the session\n   */\n  cookies?: Array<{\n    /**\n     * The name of the cookie\n     */\n    name: string;\n    /**\n     * The value of the cookie\n     */\n    value: string;\n    /**\n     * The URL of the cookie\n     */\n    url?: string;\n    /**\n     * The domain of the cookie\n     */\n    domain?: string;\n    /**\n     * The path of the cookie\n     */\n    path?: string;\n    /**\n     * Whether the cookie is secure\n     */\n    secure?: boolean;\n    /**\n     * Whether the cookie is HTTP only\n     */\n    httpOnly?: boolean;\n    /**\n     * The same site attribute of the cookie\n     */\n    sameSite?: \"Strict\" | \"Lax\" | \"None\";\n    /**\n     * The size of the cookie\n     */\n    size?: number;\n    /**\n     * The expiration date of the cookie\n     */\n    expires?: number;\n    /**\n     * The partition key of the cookie\n     */\n    partitionKey?: {\n      /**\n       * The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie.\n       */\n      topLevelSite: string;\n      /**\n       * Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.\n       */\n      hasCrossSiteAncestor: boolean;\n    };\n    /**\n     * Whether the cookie is a session cookie\n     */\n    session?: boolean;\n    /**\n     * The priority of the cookie\n     */\n    priority?: \"Low\" | \"Medium\" | \"High\";\n    /**\n     * Whether the cookie is a same party cookie\n     */\n    sameParty?: boolean;\n    /**\n     * The source scheme of the cookie\n     */\n    sourceScheme?: \"Unset\" | \"NonSecure\" | \"Secure\";\n    /**\n     * The source port of the cookie\n     */\n    sourcePort?: number;\n  }>;\n  /**\n   * Domain-specific localStorage items to initialize in the session\n   */\n  localStorage?: {\n    [key: string]: {\n      [key: string]: string;\n    };\n  };\n  /**\n   * Domain-specific sessionStorage items to initialize in the session\n   */\n  sessionStorage?: {\n    [key: string]: {\n      [key: string]: string;\n    };\n  };\n  /**\n   * Domain-specific indexedDB items to initialize in the session\n   */\n  indexedDB?: {\n    [key: string]: Array<{\n      id: number;\n      name: string;\n      data: Array<{\n        id: number;\n        name: string;\n        records: Array<{\n          key?: unknown;\n          value?: unknown;\n          blobFiles?: Array<{\n            blobNumber: number;\n            mimeType: string;\n            size: number;\n            filename?: string;\n            lastModified?: Date;\n            path?: string;\n          }>;\n        }>;\n      }>;\n    }>;\n  };\n};\n\nexport type RecordedEvents = {\n  /**\n   * Events to emit\n   */\n  events: unknown[];\n};\n\nexport type ReleaseSession = {\n  /**\n   * Unique identifier for the session\n   */\n  id: string;\n  /**\n   * Timestamp when the session started\n   */\n  createdAt: Date;\n  /**\n   * Status of the session\n   */\n  status: \"idle\" | \"live\" | \"released\" | \"failed\";\n  /**\n   * Duration of the session in milliseconds\n   */\n  duration: number;\n  /**\n   * Number of events processed in the session\n   */\n  eventCount: number;\n  /**\n   * Dimensions used for the session\n   */\n  dimensions?: {\n    width: number;\n    height: number;\n  };\n  /**\n   * Session timeout duration in milliseconds\n   */\n  timeout: number;\n  /**\n   * Amount of credits consumed by the session\n   */\n  creditsUsed: number;\n  /**\n   * URL for the session's WebSocket connection\n   */\n  websocketUrl: string;\n  /**\n   * URL for a viewing the live browser instance for the session\n   */\n  debugUrl: string;\n  /**\n   * URL for debugging the session\n   */\n  debuggerUrl: string;\n  /**\n   * URL to view session details\n   */\n  sessionViewerUrl: string;\n  /**\n   * User agent string used in the session\n   */\n  userAgent?: string;\n  /**\n   * Proxy server used for the session\n   */\n  proxy?: string;\n  /**\n   * Amount of data transmitted through the proxy\n   */\n  proxyTxBytes: number;\n  /**\n   * Amount of data received through the proxy\n   */\n  proxyRxBytes: number;\n  /**\n   * Indicates if captcha solving is enabled\n   */\n  solveCaptcha?: boolean;\n  /**\n   * Indicates if Selenium is used in the session\n   */\n  isSelenium?: boolean;\n  /**\n   * Indicates if the session was successfully released\n   */\n  success: boolean;\n};\n\nexport type SessionStreamQuery = {\n  /**\n   * Show controls in the browser iframe\n   */\n  showControls?: boolean;\n  /**\n   * Theme of the browser iframe\n   */\n  theme?: \"dark\" | \"light\";\n  /**\n   * Make the browser iframe interactive\n   */\n  interactive?: boolean;\n  /**\n   * Page ID to connect to\n   */\n  pageId?: string;\n  /**\n   * Page index (or tab index) to connect to\n   */\n  pageIndex?: string;\n};\n\n/**\n * Theme of the browser iframe\n */\nexport type theme = \"dark\" | \"light\";\n\n/**\n * Theme of the browser iframe\n */\nexport const theme = {\n  DARK: \"dark\",\n  LIGHT: \"light\",\n} as const;\n\n/**\n * HTML content for the session streamer view\n */\nexport type SessionStreamResponse = string;\n\nexport type SessionLiveDetailsResponse = {\n  sessionViewerUrl: string;\n  sessionViewerFullscreenUrl: string;\n  websocketUrl: string;\n  pages: Array<{\n    id: string;\n    url: string;\n    title: string;\n    favicon: string | null;\n  }>;\n  browserState: {\n    status: \"idle\" | \"live\" | \"released\" | \"failed\";\n    userAgent: string;\n    browserVersion: string;\n    initialDimensions: {\n      width: number;\n      height: number;\n    };\n    pageCount: number;\n  };\n};\n\nexport type LogQuerySchema = {\n  startTime?: Date;\n  endTime?: Date;\n  eventTypes?: string;\n  pageId?: string;\n  targetType?: string;\n  limit?: number;\n  offset?: number;\n};\n\nexport type LogStatsSchema = {\n  totalEvents: number;\n  oldestEvent: Date | null;\n  newestEvent: Date | null;\n  sizeBytes: number;\n};\n\nexport type LogQueryResultSchema = {\n  events: Array<{\n    [key: string]: unknown;\n  }>;\n  total: number;\n  hasMore: boolean;\n};\n\nexport type ExportLogsSchema = {\n  query?: {\n    startTime?: Date;\n    endTime?: Date;\n    eventTypes?: string;\n    pageId?: string;\n    targetType?: string;\n    limit?: number;\n    offset?: number;\n  };\n};\n\nexport type GetDevtoolsUrlSchema = {\n  pageId?: string;\n};\n\nexport type LaunchRequest = {\n  options: {\n    args?: Array<string>;\n    chromiumSandbox?: boolean;\n    devtools?: boolean;\n    downloadsPath?: string;\n    headless?: boolean;\n    ignoreDefaultArgs?: boolean | Array<string>;\n    proxyUrl?: string;\n    timeout?: number;\n    tracesDir?: string;\n  };\n  req?: unknown;\n  stealth?: boolean;\n  cookies?: unknown[];\n  userAgent?: string;\n  extensions?: Array<string>;\n  /**\n   * Deprecated\n   */\n  logSinkUrl?: string;\n  customHeaders?: {\n    [key: string]: string;\n  };\n  timezone?: string;\n  dimensions?: {\n    width: number;\n    height: number;\n  } | null;\n};\n\nexport type LaunchResponse = {\n  success: boolean;\n};\n\nexport type FileUploadRequest = {\n  /**\n   * The file to upload (binary) or URL string to download from\n   */\n  file?: unknown;\n  /**\n   * Path to the file in the storage system\n   */\n  path?: string;\n};\n\nexport type FileDetails = {\n  /**\n   * Path to the file in the storage system\n   */\n  path: string;\n  /**\n   * Size of the file in bytes\n   */\n  size: number;\n  /**\n   * Timestamp when the file was last updated\n   */\n  lastModified: Date;\n};\n\nexport type MultipleFiles = {\n  /**\n   * Array of files for the current page\n   */\n  data: Array<{\n    /**\n     * Path to the file in the storage system\n     */\n    path: string;\n    /**\n     * Size of the file in bytes\n     */\n    size: number;\n    /**\n     * Timestamp when the file was last updated\n     */\n    lastModified: Date;\n  }>;\n};\n\nexport type ScrapeData = {\n  body?: ScrapeRequest;\n};\n\nexport type ScrapeResponse2 = ScrapeResponse;\n\nexport type ScrapeError = unknown;\n\nexport type ScreenshotData = {\n  body?: ScreenshotRequest;\n};\n\nexport type ScreenshotResponse2 = ScreenshotResponse;\n\nexport type ScreenshotError = unknown;\n\nexport type PdfData = {\n  body?: PDFRequest;\n};\n\nexport type PdfResponse = PDFResponse;\n\nexport type PdfError = unknown;\n\nexport type HealthResponse = unknown;\n\nexport type HealthError = unknown;\n\nexport type LaunchBrowserSessionData = {\n  body?: CreateSession;\n};\n\nexport type LaunchBrowserSessionResponse = SessionDetails;\n\nexport type LaunchBrowserSessionError = unknown;\n\nexport type GetSessionsResponse = MultipleSessions;\n\nexport type GetSessionsError = unknown;\n\nexport type GetSessionDetailsData = {\n  path: {\n    sessionId: string;\n  };\n};\n\nexport type GetSessionDetailsResponse = SessionDetails;\n\nexport type GetSessionDetailsError = unknown;\n\nexport type GetBrowserContextData = {\n  path: {\n    sessionId: string;\n  };\n};\n\nexport type GetBrowserContextResponse = SessionContextSchema;\n\nexport type GetBrowserContextError = unknown;\n\nexport type ReleaseBrowserSessionData = {\n  path: {\n    sessionId: string;\n  };\n};\n\nexport type ReleaseBrowserSessionResponse = ReleaseSession;\n\nexport type ReleaseBrowserSessionError = unknown;\n\nexport type ReleaseBrowserSessionsResponse = ReleaseSession;\n\nexport type ReleaseBrowserSessionsError = unknown;\n\nexport type GetSessionDebuggerStreamData = {\n  query?: {\n    /**\n     * Make the browser iframe interactive\n     */\n    interactive?: boolean;\n    /**\n     * Page ID to connect to\n     */\n    pageId?: string;\n    /**\n     * Page index (or tab index) to connect to\n     */\n    pageIndex?: string;\n    /**\n     * Show controls in the browser iframe\n     */\n    showControls?: boolean;\n    /**\n     * Theme of the browser iframe\n     */\n    theme?: \"dark\" | \"light\";\n  };\n};\n\nexport type GetSessionDebuggerStreamResponse = SessionStreamResponse;\n\nexport type GetSessionDebuggerStreamError = unknown;\n\nexport type ReceiveEventsData = {\n  body?: RecordedEvents;\n};\n\nexport type ReceiveEventsResponse = unknown;\n\nexport type ReceiveEventsError = unknown;\n\nexport type GetSessionLiveDetailsData = {\n  path: {\n    id: string;\n  };\n};\n\nexport type GetSessionLiveDetailsResponse = SessionLiveDetailsResponse;\n\nexport type GetSessionLiveDetailsError = unknown;\n\nexport type ScrapeSessionData = {\n  body?: ScrapeRequest;\n};\n\nexport type ScrapeSessionResponse = ScrapeResponse;\n\nexport type ScrapeSessionError = unknown;\n\nexport type ScreenshotSessionData = {\n  body?: ScreenshotRequest;\n};\n\nexport type ScreenshotSessionResponse = ScreenshotResponse;\n\nexport type ScreenshotSessionError = unknown;\n\nexport type PdfSessionData = {\n  body?: PDFRequest;\n};\n\nexport type PdfSessionResponse = PDFResponse;\n\nexport type PdfSessionError = unknown;\n\nexport type GetDevtoolsUrlData = {\n  query?: {\n    pageId?: string;\n  };\n};\n\nexport type GetDevtoolsUrlResponse = unknown;\n\nexport type GetDevtoolsUrlError = unknown;\n\nexport type UploadFileData = {\n  body?: FileUploadRequest;\n  path: {\n    sessionId: string;\n  };\n};\n\nexport type UploadFileResponse = FileDetails;\n\nexport type UploadFileError = unknown;\n\nexport type ListFilesData = {\n  path: {\n    sessionId: string;\n  };\n};\n\nexport type ListFilesResponse = MultipleFiles;\n\nexport type ListFilesError = unknown;\n\nexport type DeleteAllFilesData = {\n  path: {\n    sessionId: string;\n  };\n};\n\nexport type DeleteAllFilesResponse = void;\n\nexport type DeleteAllFilesError = unknown;\n\nexport type DownloadFileData = {\n  path: {\n    \"*\": string;\n    sessionId: string;\n  };\n};\n\nexport type DownloadFileResponse = unknown;\n\nexport type DownloadFileError = unknown;\n\nexport type DeleteFileData = {\n  path: {\n    \"*\": string;\n    sessionId: string;\n  };\n};\n\nexport type DeleteFileResponse = void;\n\nexport type DeleteFileError = unknown;\n\nexport type DownloadArchiveData = {\n  path: {\n    sessionId: string;\n  };\n};\n\nexport type DownloadArchiveResponse = unknown;\n\nexport type DownloadArchiveError = unknown;\n\nexport type GetV1LogsQueryData = {\n  query?: {\n    endTime?: Date;\n    eventTypes?: string;\n    limit?: number;\n    offset?: number;\n    pageId?: string;\n    startTime?: Date;\n    targetType?: string;\n  };\n};\n\nexport type GetV1LogsQueryResponse = unknown;\n\nexport type GetV1LogsQueryError = unknown;\n\nexport type GetV1LogsStatsResponse = unknown;\n\nexport type GetV1LogsStatsError = unknown;\n\nexport type GetV1LogsStreamResponse = unknown;\n\nexport type GetV1LogsStreamError = unknown;\n\nexport type PostV1LogsExportData = {\n  query?: {\n    endTime?: Date;\n    eventTypes?: string;\n    limit?: number;\n    offset?: number;\n    pageId?: string;\n    startTime?: Date;\n    targetType?: string;\n  };\n};\n\nexport type PostV1LogsExportResponse = unknown;\n\nexport type PostV1LogsExportError = unknown;\n\nexport type DeleteV1LogsResponse = unknown;\n\nexport type DeleteV1LogsError = unknown;\n\nexport type ScrapeResponseTransformer = (data: any) => Promise<ScrapeResponse>;\n\nexport type ScrapeResponseModelResponseTransformer = (\n  data: any,\n) => ScrapeResponse;\n\nexport const ScrapeResponseModelResponseTransformer: ScrapeResponseModelResponseTransformer =\n  (data) => {\n    if (data?.metadata?.timestamp) {\n      data.metadata.timestamp = new Date(data.metadata.timestamp);\n    }\n    return data;\n  };\n\nexport const ScrapeResponseTransformer: ScrapeResponseTransformer = async (\n  data,\n) => {\n  ScrapeResponseModelResponseTransformer(data);\n  return data;\n};\n\nexport type LaunchBrowserSessionResponseTransformer = (\n  data: any,\n) => Promise<LaunchBrowserSessionResponse>;\n\nexport type SessionDetailsModelResponseTransformer = (\n  data: any,\n) => SessionDetails;\n\nexport const SessionDetailsModelResponseTransformer: SessionDetailsModelResponseTransformer =\n  (data) => {\n    if (data?.createdAt) {\n      data.createdAt = new Date(data.createdAt);\n    }\n    return data;\n  };\n\nexport const LaunchBrowserSessionResponseTransformer: LaunchBrowserSessionResponseTransformer =\n  async (data) => {\n    SessionDetailsModelResponseTransformer(data);\n    return data;\n  };\n\nexport type GetSessionDetailsResponseTransformer = (\n  data: any,\n) => Promise<GetSessionDetailsResponse>;\n\nexport const GetSessionDetailsResponseTransformer: GetSessionDetailsResponseTransformer =\n  async (data) => {\n    SessionDetailsModelResponseTransformer(data);\n    return data;\n  };\n\nexport type ReleaseBrowserSessionResponseTransformer = (\n  data: any,\n) => Promise<ReleaseBrowserSessionResponse>;\n\nexport type ReleaseSessionModelResponseTransformer = (\n  data: any,\n) => ReleaseSession;\n\nexport const ReleaseSessionModelResponseTransformer: ReleaseSessionModelResponseTransformer =\n  (data) => {\n    if (data?.createdAt) {\n      data.createdAt = new Date(data.createdAt);\n    }\n    return data;\n  };\n\nexport const ReleaseBrowserSessionResponseTransformer: ReleaseBrowserSessionResponseTransformer =\n  async (data) => {\n    ReleaseSessionModelResponseTransformer(data);\n    return data;\n  };\n\nexport type ReleaseBrowserSessionsResponseTransformer = (\n  data: any,\n) => Promise<ReleaseBrowserSessionsResponse>;\n\nexport const ReleaseBrowserSessionsResponseTransformer: ReleaseBrowserSessionsResponseTransformer =\n  async (data) => {\n    ReleaseSessionModelResponseTransformer(data);\n    return data;\n  };\n\nexport type ScrapeSessionResponseTransformer = (\n  data: any,\n) => Promise<ScrapeSessionResponse>;\n\nexport const ScrapeSessionResponseTransformer: ScrapeSessionResponseTransformer =\n  async (data) => {\n    ScrapeResponseModelResponseTransformer(data);\n    return data;\n  };\n\nexport type UploadFileResponseTransformer = (\n  data: any,\n) => Promise<UploadFileResponse>;\n\nexport type FileDetailsModelResponseTransformer = (data: any) => FileDetails;\n\nexport const FileDetailsModelResponseTransformer: FileDetailsModelResponseTransformer =\n  (data) => {\n    if (data?.lastModified) {\n      data.lastModified = new Date(data.lastModified);\n    }\n    return data;\n  };\n\nexport const UploadFileResponseTransformer: UploadFileResponseTransformer =\n  async (data) => {\n    FileDetailsModelResponseTransformer(data);\n    return data;\n  };\n"
  },
  {
    "path": "ui/src/styles/common.styles.tsx",
    "content": "import styled from \"styled-components\";\n\nexport const Container = styled.div`\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  flex: 1;\n`;\n"
  },
  {
    "path": "ui/src/styles/theme.ts",
    "content": "import {\n  redDark,\n  redDarkA,\n  greenDark,\n  greenDarkA,\n  amberDark,\n  amberDarkA,\n  indigoDark,\n  indigoDarkA,\n  slateDark,\n  slateDarkA,\n  skyDark,\n  skyDarkA,\n} from \"@radix-ui/colors\";\n\ntype ReplaceKeyPrefix<\n  Obj,\n  OldPrefix extends string,\n  NewPrefix extends string,\n> = {\n  [K in keyof Obj as K extends `${OldPrefix}${infer Rest}`\n    ? `${NewPrefix}${Rest}`\n    : never]: Obj[K];\n};\nfunction renameKeysWithPrefix<\n  Obj extends Record<string, string>,\n  OldPrefix extends string,\n  NewPrefix extends string,\n>(\n  obj: Obj,\n  oldPrefix: OldPrefix,\n  newPrefix: NewPrefix,\n): ReplaceKeyPrefix<Obj, OldPrefix, NewPrefix> {\n  const result: any = {};\n\n  Object.keys(obj).forEach((key) => {\n    const newKey = key.replace(oldPrefix, newPrefix);\n    result[newKey] = obj[key];\n  });\n\n  return result;\n}\n\nexport const theme = {\n  colors: {\n    white: \"#FFFFFF\",\n    black: \"#1C2024\",\n    panelDefault: \"#1D1D21B2\",\n    ...renameKeysWithPrefix(indigoDark, \"indigo\", \"accent\"),\n    ...renameKeysWithPrefix(indigoDarkA, \"indigoA\", \"accentAlpha\"),\n    ...renameKeysWithPrefix(slateDark, \"slate\", \"neutral\"),\n    ...renameKeysWithPrefix(slateDarkA, \"slateA\", \"neutralAlpha\"),\n    ...renameKeysWithPrefix(redDark, \"red\", \"error\"),\n    ...renameKeysWithPrefix(redDarkA, \"redA\", \"errorAlpha\"),\n    ...renameKeysWithPrefix(greenDark, \"green\", \"success\"),\n    ...renameKeysWithPrefix(greenDarkA, \"greenA\", \"successAlpha\"),\n    ...renameKeysWithPrefix(amberDark, \"amber\", \"warning\"),\n    ...renameKeysWithPrefix(amberDarkA, \"amberA\", \"warningAlpha\"),\n    ...renameKeysWithPrefix(skyDark, \"sky\", \"info\"),\n    ...renameKeysWithPrefix(skyDarkA, \"skyA\", \"infoAlpha\"),\n  },\n};\n\nexport type Theme = typeof theme;\n"
  },
  {
    "path": "ui/src/types/cdp.ts",
    "content": "export enum ProtocolCommands {\n  \"Input.dispatchKeyEvent\" = \"Input.dispatchKeyEvent\",\n  \"Input.emulateTouchFromMouseEvent\" = \"Input.emulateTouchFromMouseEvent\",\n}\n\nexport enum HostCommands {\n  \"start\" = \"start\",\n  \"run\" = \"run\",\n  \"close\" = \"close\",\n  \"setViewport\" = \"setViewport\",\n}\n\nexport enum WorkerCommands {\n  \"startComplete\" = \"startComplete\",\n  \"runComplete\" = \"runComplete\",\n  \"screencastFrame\" = \"screencastFrame\",\n  \"browserClose\" = \"browserClose\",\n  \"error\" = \"error\",\n}\n\nexport interface Message {\n  command: string;\n  data: any;\n}\n"
  },
  {
    "path": "ui/src/types/props.ts",
    "content": "export interface IconProps {\n  width?: number;\n  height?: number;\n  color?: string;\n}\n"
  },
  {
    "path": "ui/src/utils/formatting.ts",
    "content": "export function formatDuration(durationInMs: number): string {\n  const totalSeconds = Math.floor(durationInMs / 1000);\n  const hours = Math.floor(totalSeconds / 3600);\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\n  const seconds = totalSeconds % 60;\n\n  return `${hours}:${minutes.toString().padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}`;\n}\n\nexport const msToMinutes = (ms: number) => Math.ceil(ms / 60000);\n"
  },
  {
    "path": "ui/src/utils/toasts.ts",
    "content": "import { toast } from \"@/hooks/use-toast\";\n\nexport const copyText = (text: string, valueCopied?: string) => {\n  navigator.clipboard.writeText(text);\n  toast({\n    title: valueCopied\n      ? `${valueCopied} copied to clipboard`\n      : \"Text copied to clipboard\",\n    className: \"bg-[var(--gray-1)] border-none text-[var(--gray-12)] px-8 py-6\",\n    duration: 700,\n  });\n};\n"
  },
  {
    "path": "ui/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "ui/tailwind.config.js",
    "content": "import tailwindcssAnimate from \"tailwindcss-animate\";\n\n/** @type {import('tailwindcss').Config} */\nexport default {\n    darkMode: [\"class\"],\n    content: [\"./index.html\", \"./src/**/*.{js,ts,jsx,tsx}\"],\n    theme: {\n        extend: {\n            borderRadius: {\n                lg: 'var(--radius)',\n                md: 'calc(var(--radius) - 2px)',\n                sm: 'calc(var(--radius) - 4px)'\n            },\n            colors: {\n                background: 'hsl(var(--background))',\n                foreground: 'hsl(var(--foreground))',\n                card: {\n                    DEFAULT: 'hsl(var(--card))',\n                    foreground: 'hsl(var(--card-foreground))'\n                },\n                popover: {\n                    DEFAULT: 'hsl(var(--popover))',\n                    foreground: 'hsl(var(--popover-foreground))'\n                },\n                primary: {\n                    DEFAULT: 'hsl(var(--primary))',\n                    foreground: 'hsl(var(--primary-foreground))'\n                },\n                secondary: {\n                    DEFAULT: 'hsl(var(--secondary))',\n                    foreground: 'hsl(var(--secondary-foreground))'\n                },\n                muted: {\n                    DEFAULT: 'hsl(var(--muted))',\n                    foreground: 'hsl(var(--muted-foreground))'\n                },\n                accent: {\n                    DEFAULT: 'hsl(var(--accent))',\n                    foreground: 'hsl(var(--accent-foreground))'\n                },\n                destructive: {\n                    DEFAULT: 'hsl(var(--destructive))',\n                    foreground: 'hsl(var(--destructive-foreground))'\n                },\n                border: 'hsl(var(--border))',\n                input: 'hsl(var(--input))',\n                ring: 'hsl(var(--ring))',\n                chart: {\n                    '1': 'hsl(var(--chart-1))',\n                    '2': 'hsl(var(--chart-2))',\n                    '3': 'hsl(var(--chart-3))',\n                    '4': 'hsl(var(--chart-4))',\n                    '5': 'hsl(var(--chart-5))'\n                }\n            }\n        }\n    },\n    plugins: [tailwindcssAnimate],\n};\n"
  },
  {
    "path": "ui/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": false,\n    \"jsx\": \"react-jsx\",\n    \"outDir\": \"./dist\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noImplicitAny\": false,\n\n    /* Absolute Imports :) */\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"],\n      \"@steel-client\": [\"./src/steel-client/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "ui/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"noImplicitAny\": false,\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "ui/vercel.json",
    "content": "{\n    \"rewrites\": [\n        {\n            \"source\": \"/(.*)\",\n            \"destination\": \"/\"\n        }\n    ]\n}"
  },
  {
    "path": "ui/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport path, { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [react()],\n  resolve: {\n    alias: {\n      \"@\": path.resolve(dirname(fileURLToPath(import.meta.url)), \"./src\"),\n    },\n  },\n  server: {\n    proxy: {\n      \"/api\": {\n        target: \"http://localhost:3000\",\n        changeOrigin: true,\n        rewrite: (path) => path.replace(/^\\/api/, \"\"),\n      },\n      \"/ws\": {\n        target: \"ws://localhost:3000\",\n        ws: true,\n        changeOrigin: true,\n        rewrite: (path) => path.replace(/^\\/ws/, \"\"),\n      },\n    },\n  },\n});\n"
  }
]