Repository: steel-dev/steel-browser Branch: main Commit: c1a55edbb5f1 Files: 281 Total size: 54.9 MB Directory structure: gitextract_z2lghh3p/ ├── .dockerignore ├── .env.example ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ ├── feature_request.md │ │ └── question.md │ ├── auto-assign.yml │ ├── labeler.yml │ ├── labels.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── auto-assign.yml │ ├── build-docker.yml │ ├── check-build.yml │ ├── pr-checks.yml │ ├── release.yml │ └── welcome.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── api/ │ ├── .dockerignore │ ├── .env.example │ ├── .gitattributes │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── .puppeteerrc.cjs │ ├── Dockerfile │ ├── entrypoint.sh │ ├── extensions/ │ │ └── recorder/ │ │ ├── .gitignore │ │ ├── manifest.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── background.js │ │ │ └── inject.js │ │ └── webpack.config.mjs │ ├── nginx.conf │ ├── openapi/ │ │ ├── generate.ts │ │ └── schemas.json │ ├── package.json │ ├── selenium/ │ │ ├── driver/ │ │ │ ├── LICENSE.chromedriver │ │ │ ├── THIRD_PARTY_NOTICES.chromedriver │ │ │ └── chromedriver2 │ │ └── server/ │ │ └── selenium-server.jar │ ├── src/ │ │ ├── config.ts │ │ ├── env.ts │ │ ├── index.ts │ │ ├── modules/ │ │ │ ├── actions/ │ │ │ │ ├── actions.controller.ts │ │ │ │ ├── actions.routes.ts │ │ │ │ └── actions.schema.ts │ │ │ ├── cdp/ │ │ │ │ ├── cdp.routes.ts │ │ │ │ └── cdp.schemas.ts │ │ │ ├── files/ │ │ │ │ ├── files.controller.ts │ │ │ │ ├── files.routes.ts │ │ │ │ └── files.schema.ts │ │ │ ├── logs/ │ │ │ │ ├── logs.routes.ts │ │ │ │ └── logs.schema.ts │ │ │ ├── selenium/ │ │ │ │ ├── selenium.routes.ts │ │ │ │ └── selenium.schema.ts │ │ │ └── sessions/ │ │ │ ├── sessions.controller.ts │ │ │ ├── sessions.routes.ts │ │ │ └── sessions.schema.ts │ │ ├── plugins/ │ │ │ ├── browser-session.ts │ │ │ ├── browser-socket/ │ │ │ │ ├── browser-socket.ts │ │ │ │ ├── casting.handler.ts │ │ │ │ └── handlers/ │ │ │ │ ├── cast.handler.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logs.handler.ts │ │ │ │ ├── pageId.handler.ts │ │ │ │ └── recording.handler.ts │ │ │ ├── browser.ts │ │ │ ├── custom-body-parser.ts │ │ │ ├── file-storage.ts │ │ │ ├── request-logger.ts │ │ │ ├── scalar-theme.ts │ │ │ ├── schemas.ts │ │ │ ├── selenium.ts │ │ │ └── ui-plugin.ts │ │ ├── routes.ts │ │ ├── scripts/ │ │ │ ├── fingerprint.js │ │ │ └── index.ts │ │ ├── services/ │ │ │ ├── cdp/ │ │ │ │ ├── cdp.service.ts │ │ │ │ ├── errors/ │ │ │ │ │ └── launch-errors.ts │ │ │ │ ├── instrumentation/ │ │ │ │ │ ├── browser-logger.test.ts │ │ │ │ │ ├── browser-logger.ts │ │ │ │ │ ├── cdp-events.ts │ │ │ │ │ ├── extension-events.ts │ │ │ │ │ ├── page-console.ts │ │ │ │ │ ├── page-events.ts │ │ │ │ │ ├── storage/ │ │ │ │ │ │ ├── duckdb-storage.ts │ │ │ │ │ │ ├── in-memory-storage.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── log-storage.interface.ts │ │ │ │ │ │ └── safe-json.ts │ │ │ │ │ ├── target-manager.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── utils.ts │ │ │ │ │ └── worker-events.ts │ │ │ │ ├── plugins/ │ │ │ │ │ ├── core/ │ │ │ │ │ │ ├── base-plugin.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── plugin-manager.ts │ │ │ │ │ └── pptr-extensions.d.ts │ │ │ │ └── utils/ │ │ │ │ ├── error-handlers.ts │ │ │ │ └── validation.ts │ │ │ ├── context/ │ │ │ │ ├── chrome-context.service.ts │ │ │ │ └── types.ts │ │ │ ├── file.service.ts │ │ │ ├── leveldb/ │ │ │ │ ├── localstorage.ts │ │ │ │ └── sessionstorage.ts │ │ │ ├── selenium.service.ts │ │ │ ├── session.service.ts │ │ │ ├── timezone-fetcher.service.ts │ │ │ └── websocket-registry.service.ts │ │ ├── steel-browser-plugin.ts │ │ ├── telemetry/ │ │ │ ├── noop.ts │ │ │ └── tracer.ts │ │ ├── templates/ │ │ │ └── live-session-streamer.ejs │ │ ├── types/ │ │ │ ├── browser.ts │ │ │ ├── casting.ts │ │ │ ├── enums.ts │ │ │ ├── fastify.d.ts │ │ │ ├── index.ts │ │ │ ├── turndown.d.ts │ │ │ └── websocket.ts │ │ └── utils/ │ │ ├── browser.ts │ │ ├── casting.ts │ │ ├── context.ts │ │ ├── errors.ts │ │ ├── extensions.ts │ │ ├── leveldb.ts │ │ ├── logging.ts │ │ ├── passthough-proxy.ts │ │ ├── proxy.ts │ │ ├── requests.ts │ │ ├── retry.ts │ │ ├── schema.ts │ │ ├── scrape/ │ │ │ ├── cleanHtml.ts │ │ │ ├── htmlToMarkdown.ts │ │ │ ├── index.ts │ │ │ ├── pdfToHtml.ts │ │ │ ├── plugins/ │ │ │ │ ├── highlightedCodeBlock.ts │ │ │ │ ├── inlineLink.ts │ │ │ │ ├── strikethrough.ts │ │ │ │ ├── table.ts │ │ │ │ ├── taskListItems.ts │ │ │ │ └── utilities.ts │ │ │ ├── readability.ts │ │ │ ├── safeGoTo.ts │ │ │ └── transformHtml.ts │ │ ├── size.ts │ │ ├── text.ts │ │ └── url.ts │ ├── tsconfig.json │ └── tsconfig.test.json ├── commitlint.config.cjs ├── docker-compose.dev.yml ├── docker-compose.yml ├── docs/ │ ├── ARCHITECTURE.md │ ├── DEVELOPMENT_SETUP.md │ ├── PLUGIN_DEVELOPMENT.md │ ├── README.md │ └── TROUBLESHOOTING.md ├── nginx.conf ├── package.json ├── render.yaml ├── repl/ │ ├── README.md │ ├── package.json │ └── src/ │ └── script.ts └── ui/ ├── .dockerignore ├── .eslintrc.cjs ├── .gitignore ├── Dockerfile ├── README.md ├── components.json ├── entrypoint.sh ├── index.html ├── nginx.conf.template ├── openapi-ts.config.ts ├── package.json ├── postcss.config.js ├── src/ │ ├── App.tsx │ ├── components/ │ │ ├── badges/ │ │ │ ├── proxy-badge.tsx │ │ │ ├── user-agent-badge.tsx │ │ │ └── websocket-url-badge.tsx │ │ ├── header/ │ │ │ ├── header.tsx │ │ │ └── index.tsx │ │ ├── icons/ │ │ │ ├── ChromeIcon.tsx │ │ │ ├── DeleteIcon.tsx │ │ │ ├── GlobeIcon.tsx │ │ │ ├── GlowingGreenDot.tsx │ │ │ ├── KeyIcon.tsx │ │ │ ├── LoadingSpinner.tsx │ │ │ ├── NinjaIcon.tsx │ │ │ ├── SessionIcon.tsx │ │ │ └── SettingsIcon.tsx │ │ ├── illustrations/ │ │ │ ├── command-line.tsx │ │ │ └── globe.tsx │ │ ├── loading/ │ │ │ ├── Loading.styles.tsx │ │ │ ├── Loading.tsx │ │ │ └── index.tsx │ │ ├── sessions/ │ │ │ ├── release-session-dialog.tsx │ │ │ ├── session-console/ │ │ │ │ ├── index.tsx │ │ │ │ ├── session-details.tsx │ │ │ │ ├── session-devtools.tsx │ │ │ │ └── session-logs.tsx │ │ │ └── session-viewer/ │ │ │ ├── empty-state.tsx │ │ │ ├── example-events/ │ │ │ │ ├── example-events.json │ │ │ │ └── test.json │ │ │ ├── index.tsx │ │ │ ├── live-empty-state.tsx │ │ │ ├── session-viewer-controls.css │ │ │ └── session-viewer.tsx │ │ ├── theme-provider.tsx │ │ └── ui/ │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── toast.tsx │ │ └── toaster.tsx │ ├── containers/ │ │ └── session-container.tsx │ ├── contexts/ │ │ └── sessions-context/ │ │ ├── index.tsx │ │ ├── sessions-context.tsx │ │ └── sessions-context.types.ts │ ├── env.ts │ ├── fonts/ │ │ ├── Geist/ │ │ │ ├── Geist-Black.otf │ │ │ ├── Geist-Bold.otf │ │ │ ├── Geist-Light.otf │ │ │ ├── Geist-Medium.otf │ │ │ ├── Geist-Regular.otf │ │ │ ├── Geist-SemiBold.otf │ │ │ ├── Geist-Thin.otf │ │ │ ├── Geist-UltraBlack.otf │ │ │ ├── Geist-UltraLight.otf │ │ │ └── LICENSE.TXT │ │ └── GeistMono/ │ │ ├── GeistMono-Black.otf │ │ ├── GeistMono-Bold.otf │ │ ├── GeistMono-Light.otf │ │ ├── GeistMono-Medium.otf │ │ ├── GeistMono-Regular.otf │ │ ├── GeistMono-SemiBold.otf │ │ ├── GeistMono-Thin.otf │ │ ├── GeistMono-UltraBlack.otf │ │ ├── GeistMono-UltraLight.otf │ │ └── LICENSE.TXT │ ├── hooks/ │ │ ├── use-sessions-context.ts │ │ └── use-toast.ts │ ├── index.css │ ├── lib/ │ │ ├── query-client.ts │ │ └── utils.ts │ ├── main.tsx │ ├── root-layout.tsx │ ├── steel-client/ │ │ ├── index.ts │ │ ├── schemas.gen.ts │ │ ├── services.gen.ts │ │ └── types.gen.ts │ ├── styles/ │ │ ├── common.styles.tsx │ │ └── theme.ts │ ├── types/ │ │ ├── cdp.ts │ │ └── props.ts │ ├── utils/ │ │ ├── formatting.ts │ │ └── toasts.ts │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vercel.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ **/node_modules/ build/ **/.env **/.env.local Dockerfile docker-compose.yml api/extensions/**/dist ================================================ FILE: .env.example ================================================ VITE_API_URL=http://HOST:3000 VITE_WS_URL=ws://HOST:3000 ================================================ FILE: .gitattributes ================================================ * text=auto *.sh text eol=lf *.conf text eol=lf ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[BUG] TITLE" labels: '' assignees: '' --- Issue tracker is **ONLY** used for reporting bugs. New features should be discussed in our [Discord server](https://discord.gg/steel-dev). **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 💬 Discord Community url: https://discord.gg/steel-dev about: Join our Discord server for real-time help, discussions, and community support - name: 📚 Documentation url: https://docs.steel.dev/ about: Check our comprehensive documentation for guides and API reference - name: 🍳 Steel Cookbook url: https://github.com/steel-dev/steel-cookbook about: Browse code examples and common use cases - name: 🔒 Security Issues url: mailto:security@steel.dev about: Please report security vulnerabilities privately via email - name: 🤖 Automated Issue Creation url: https://github.com/steel-dev/steel-browser/issues/new/choose about: Use our templates for better issue tracking ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for Steel Browser title: "[FEATURE] " labels: 'enhancement' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Use case** Describe the specific use case or scenario where this feature would be helpful. **Implementation ideas (optional)** If you have ideas about how this could be implemented, please share them. **Additional context** Add any other context, screenshots, or examples about the feature request here. **Would you be willing to contribute this feature?** - [ ] Yes, I'd like to work on this - [ ] Yes, with guidance from maintainers - [ ] No, but I'd be happy to test it - [ ] No, just suggesting the idea ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: Ask a question about using Steel Browser title: "[QUESTION] " labels: 'question' assignees: '' --- **Before asking your question** - [ ] I've checked the [documentation](https://docs.steel.dev/) - [ ] I've searched existing issues - [ ] I've looked at the [Steel Cookbook](https://github.com/steel-dev/steel-cookbook) for examples **What are you trying to do?** A clear description of what you're trying to accomplish. **What have you tried?** Describe what you've already attempted and what happened. **Code example (if applicable)** ```typescript // Include relevant code snippets here ``` **Environment** - Steel Browser version: - Node.js version: - Operating System: - Browser: Chrome/Chromium version **Additional context** Add any other context, error messages, or screenshots that might help. --- 💡 **Tip**: For real-time help and community discussion, consider joining our [Discord server](https://discord.gg/steel-dev)! ================================================ FILE: .github/auto-assign.yml ================================================ # Auto-assign configuration for Steel Browser # This file is used by the kentaro-m/auto-assign-action # Add reviewers to pull requests addReviewers: true # Add assignees to pull requests addAssignees: false # Number of reviewers to add to each pull request numberOfReviewers: 1 # Reviewers to be added to pull requests (GitHub usernames) reviewers: - fukouda # Skip draft pull requests skipDraftPR: true # Only assign reviewers if no reviewers are already assigned assignWhenNoReviewers: true ================================================ FILE: .github/labeler.yml ================================================ # GitHub Labeler Configuration for Steel Browser # This file is used by the actions/labeler@v5 action in pr-checks.yml # Component labels based on file paths api: - changed-files: - any-glob-to-any-file: 'api/**/*' ui: - changed-files: - any-glob-to-any-file: 'ui/**/*' documentation: - changed-files: - any-glob-to-any-file: - 'docs/**/*' - '*.md' - 'README.md' - 'CONTRIBUTING.md' plugin-system: - changed-files: - any-glob-to-any-file: 'api/src/services/cdp/plugins/**/*' cdp: - changed-files: - any-glob-to-any-file: - 'api/src/services/cdp/**/*' - 'api/src/modules/cdp/**/*' session-management: - changed-files: - any-glob-to-any-file: - 'api/src/services/session.service.ts' - 'api/src/modules/sessions/**/*' file-storage: - changed-files: - any-glob-to-any-file: - 'api/src/services/file.service.ts' - 'api/src/modules/files/**/*' docker: - changed-files: - any-glob-to-any-file: - 'docker-compose*.yml' - 'api/Dockerfile' - 'ui/Dockerfile' - '.dockerignore' dependencies: - changed-files: - any-glob-to-any-file: - 'package-lock.json' - '**/package-lock.json' - 'package.json' - '**/package.json' github-actions: - changed-files: - any-glob-to-any-file: - '.github/workflows/**/*' - '.github/**/*' testing: - changed-files: - any-glob-to-any-file: - '**/*.test.ts' - '**/*.test.js' - '**/*.spec.ts' - '**/*.spec.js' - 'tsconfig.test.json' ================================================ FILE: .github/labels.yml ================================================ # GitHub Labels Configuration for Steel Browser # This file can be used with the github-labels CLI tool to sync labels # Type labels - name: "bug" color: "d73a4a" description: "Something isn't working" - name: "enhancement" color: "a2eeef" description: "New feature or request" - name: "documentation" color: "0075ca" description: "Improvements or additions to documentation" - name: "question" color: "d876e3" description: "Further information is requested" # Priority labels - name: "priority: critical" color: "b60205" description: "Critical issue that needs immediate attention" - name: "priority: high" color: "d93f0b" description: "High priority issue" - name: "priority: medium" color: "fbca04" description: "Medium priority issue" - name: "priority: low" color: "0e8a16" description: "Low priority issue" # Difficulty labels - name: "good first issue" color: "7057ff" description: "Good for newcomers" - name: "help wanted" color: "008672" description: "Extra attention is needed" - name: "difficulty: easy" color: "c2e0c6" description: "Easy to implement" - name: "difficulty: medium" color: "fef2c0" description: "Moderate difficulty" - name: "difficulty: hard" color: "f9d0c4" description: "Hard to implement" # Component labels - name: "api" color: "1f77b4" description: "Related to the API backend" - name: "ui" color: "ff7f0e" description: "Related to the frontend UI" - name: "plugin-system" color: "2ca02c" description: "Related to the plugin architecture" - name: "cdp" color: "d62728" description: "Related to Chrome DevTools Protocol" - name: "session-management" color: "9467bd" description: "Related to browser session handling" - name: "file-storage" color: "8c564b" description: "Related to file upload/download functionality" - name: "docker" color: "0db7ed" description: "Related to Docker configuration" - name: "testing" color: "17becf" description: "Related to testing infrastructure" # Status labels - name: "status: blocked" color: "b60205" description: "Blocked by another issue or external dependency" - name: "status: in progress" color: "fbca04" description: "Currently being worked on" - name: "status: needs review" color: "0052cc" description: "Needs code review" - name: "status: needs testing" color: "1d76db" description: "Needs testing before merge" - name: "status: ready to merge" color: "0e8a16" description: "Ready to be merged" # Breaking change labels - name: "breaking change" color: "b60205" description: "Introduces breaking changes" - name: "backwards compatible" color: "0e8a16" description: "Backwards compatible changes" # Special labels - name: "duplicate" color: "cfd3d7" description: "This issue or pull request already exists" - name: "invalid" color: "e4e669" description: "This doesn't seem right" - name: "wontfix" color: "ffffff" description: "This will not be worked on" - name: "dependencies" color: "0366d6" description: "Pull requests that update a dependency file" - name: "security" color: "d73a4a" description: "Security-related issue" - name: "performance" color: "ff9500" description: "Performance improvement" - name: "refactor" color: "5319e7" description: "Code refactoring" - name: "chore" color: "fef2c0" description: "Maintenance tasks" ================================================ FILE: .github/pull_request_template.md ================================================ ## Description Brief description of the changes in this PR. ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [ ] Test addition/update ## Related Issues Closes #(issue number) Related to #(issue number) ## Changes Made - [ ] List specific changes made - [ ] Include any new files or major modifications - [ ] Mention any removed functionality ## Testing - [ ] I have tested this locally - [ ] I have added/updated unit tests - [ ] I have added/updated integration tests - [ ] I have tested with Docker - [ ] All existing tests pass ## Documentation - [ ] I have updated relevant documentation - [ ] I have added JSDoc comments for new public APIs - [ ] I have updated the README if needed - [ ] I have updated the CHANGELOG if needed ## Code Quality - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] My changes generate no new warnings or errors ## Breaking Changes If this is a breaking change, please describe: 1. What breaks: 2. How users should migrate: 3. Why this change is necessary: ## Screenshots (if applicable) Include screenshots or GIFs for UI changes. ## Additional Notes Any additional information, concerns, or context for reviewers. --- ## Reviewer Checklist - [ ] Code follows project conventions and style - [ ] Changes are well-tested - [ ] Documentation is updated appropriately - [ ] No security concerns - [ ] Performance impact is acceptable - [ ] Breaking changes are properly documented ================================================ FILE: .github/workflows/auto-assign.yml ================================================ # .github/workflows/auto-assign.yml name: Auto Assign on: pull_request: types: [opened, ready_for_review] jobs: assign: runs-on: ubuntu-latest steps: - name: Auto Assign uses: kentaro-m/auto-assign-action@v1.2.5 with: configuration-path: ".github/auto-assign.yml" ================================================ FILE: .github/workflows/build-docker.yml ================================================ name: Build and Push Latest Docker Image to GHCR on: push: branches: - main jobs: push_to_registry: name: Build and Push Latest Docker Image to GHCR runs-on: ubuntu-latest steps: - name: Check out the repo uses: actions/checkout@v4 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v3 - name: Log in to the Container registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ secrets.GH_USERNAME }} password: ${{ secrets.GH_TOKEN }} - name: Build and push the latest Steel Browser image run: | docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/steel-dev/steel-browser:latest . - name: Build and push the latest Steel Browser API image run: | docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/steel-dev/steel-browser-api:latest . -f ./api/Dockerfile - name: Build and push the latest Steel Browser UI image run: | docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/steel-dev/steel-browser-ui:latest . -f ./ui/Dockerfile ================================================ FILE: .github/workflows/check-build.yml ================================================ name: Check Docker Build on: pull_request: branches: - main jobs: check-docker-build: name: Check Docker Build runs-on: ubuntu-latest steps: - name: Check out the repo uses: actions/checkout@v4 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v3 - name: Build the latest Steel Browser image run: | docker build -t steel-browser -f ./Dockerfile . - name: Build the latest Steel Browser API image run: | docker build -t steel-browser-api -f ./api/Dockerfile . - name: Build the latest Steel Browser UI image run: | docker build -t steel-browser-ui -f ./ui/Dockerfile . ================================================ FILE: .github/workflows/pr-checks.yml ================================================ # .github/workflows/pr-checks.yml name: PR Quality Checks on: pull_request_target: branches: [main] permissions: contents: read pull-requests: write jobs: validate-pr: runs-on: ubuntu-latest steps: # Check PR title follows conventional commits - name: Validate PR Title uses: amannn/action-semantic-pull-request@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Auto-assign labels based on files changed - name: Label PR uses: actions/labeler@v5 # Check for breaking changes - name: Check Breaking Changes env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Get list of changed files in this PR CHANGED_FILES=$(gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' | tr '\n' ' ') echo "Changed files: $CHANGED_FILES" # Check for potential breaking changes in specific files if echo "$CHANGED_FILES" | grep -E "(api/src/types/|api/src/steel-browser-plugin.ts|api/src/services/cdp/plugins/core/)"; then echo "::warning::Potential breaking changes detected in core APIs" fi # Check for package.json changes if echo "$CHANGED_FILES" | grep -E "package\.json$"; then echo "::warning::Package.json changes detected - review dependencies carefully" fi ================================================ FILE: .github/workflows/release.yml ================================================ name: Automatic Release on: push: branches: - main jobs: release: runs-on: ubuntu-latest permissions: contents: write packages: write steps: - name: Checkout code uses: actions/checkout@v4 # Automatic semantic version bump (major/minor/patch from commit messages) - name: Bump version and push tag id: bump_version uses: phips28/gh-action-bump-version@v11.0.3 with: tag-prefix: "v" tag-suffix: "-beta" skip-commit: true patch-wording: "patch,fix,fixes,docs,feat,feature,minor" minor-wording: "" major-wording: "breaking,breaking-change,major" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Generate changelog file from previous commits - name: Generate changelog uses: mikepenz/release-changelog-builder-action@v5 id: changelog with: mode: "COMMIT" configurationJson: | { "template": "#{{CHANGELOG}}", "commit_template": "- [`#{{MERGE_SHA_SUBSTRING}}`](${{ github.server_url }}/${{ github.repository }}/commit/#{{MERGE_SHA}}): #{{TITLE}} (@#{{AUTHOR}})", "custom_placeholders": [ { "name": "MERGE_SHA_SUBSTRING", "source": "MERGE_SHA", "transformer": { "pattern": "^(.{6})", "method": "regexr", "target": "$1" } } ], "categories": [ { "title": "## Improvements", "labels": [ "feat", "feature" ] }, { "title": "## Bug Fixes", "labels": [ "fix", "bug" ] }, { "title": "## Documentation", "labels": [ "docs" ] }, { "title": "## Housekeeping", "labels": [] } ], "sort": { "order": "ASC", "on_property": "mergedAt" }, "label_extractor": [ { "pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\\([\\w\\-\\.]+\\))?(!)?: ([\\w ])+([\\s\\S]*)", "on_property": "title", "target": "$1" } ] } toTag: ${{ steps.bump_version.outputs.newTag }} fromTag: "" # Create automatic GitHub release - name: Create GitHub Release uses: ncipollo/release-action@v1.18.0 with: token: "${{ secrets.GITHUB_TOKEN }}" tag: ${{ steps.bump_version.outputs.newTag }} prerelease: false name: "Release ${{ steps.bump_version.outputs.newTag }}" body: | ${{ steps.changelog.outputs.changelog }} --- ![release-image](https://raw.githubusercontent.com/steel-dev/.github/refs/heads/main/profile/github_hero.png) ## Come Hang Out - Questions? Join us on [Discord](https://discord.gg/gPpvhNvc5R) - Found a bug? Open an issue on [GitHub](https://github.com/steel-dev/steel-browser/issues) ================================================ FILE: .github/workflows/welcome.yml ================================================ # .github/workflows/welcome.yml name: Welcome on: issues: types: [opened] pull_request: types: [opened] jobs: welcome: runs-on: ubuntu-latest steps: - name: Welcome new contributors uses: actions/first-interaction@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: | Thanks for opening your first issue! 🎉 Please check our [Contributing Guide](CONTRIBUTING.md) and [Troubleshooting Guide](docs/TROUBLESHOOTING.md). pr-message: | Thanks for your first contribution! 🚀 Please ensure you've followed our [Contributing Guidelines](CONTRIBUTING.md). ================================================ FILE: .gitignore ================================================ node_modules .pnp .pnp.js coverage .DS_Store *.pem npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* .env .env.local .env.development.local .test.env.local .env.production.local !.env.example production.env .turbo build db/data *.tsbuildinfo out .idea *.env dist .aider* extensions/* ================================================ FILE: .husky/commit-msg ================================================ npx --no-install commitlint --edit "$1" ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" # Code formatting and linting echo "🎨 Running code formatting..." npm run pretty -w api echo "🔍 Running linting..." npm run lint -w ui --fix # Type checking echo "🔧 Running type checking..." npm run build # Add formatted files back to staging git add -u echo "✅ Pre-commit checks passed!" ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Steel Browser Welcome 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. ## 🚀 Quick Start ### Prerequisites - **Node.js**: Version 22 or higher - **npm**: Version 10 or higher - **Docker**: For containerized development (optional but recommended) - **Git**: For version control - **Chrome/Chromium**: Required for browser automation ### Development Setup 1. **Fork and Clone** ```bash git clone https://github.com/steel-dev/steel-browser.git cd steel-browser ``` 2. **Install Dependencies** ```bash npm install ``` 3. **Start Development Environment** ```bash # Start both API and UI in development mode npm run dev # Or start individually: npm run dev -w api # API server on http://localhost:3000 npm run dev -w ui # UI server on http://localhost:5173 ``` 4. **Verify Setup** - API: Visit http://localhost:3000/documentation - UI: Visit http://localhost:5173 - Test REPL: `cd repl && npm start` ### Docker Development (Alternative) ```bash # Build and run with Docker Compose docker-compose -f docker-compose.dev.yml up --build # Or use production images docker-compose up ``` ## 📁 Project Structure ``` steel-browser/ ├── api/ # Backend API (Fastify + Puppeteer) │ ├── src/ │ │ ├── modules/ # API modules (actions, sessions, etc.) │ │ ├── plugins/ # Fastify plugins │ │ ├── services/ # Core services (CDP, file, session) │ │ └── types/ # TypeScript type definitions │ └── extensions/ # Browser extensions (must pass name as param to session creation) ├── ui/ # Frontend UI (React + Vite) │ └── src/ │ ├── components/ # Reusable UI components │ ├── containers/ # Page containers │ └── contexts/ # React contexts ├── repl/ # Interactive REPL for testing └── docs/ # Documentation ``` ## 🏗️ Architecture Overview Steel Browser follows a plugin-based architecture: ### Core Components 1. **Steel Browser Plugin** (`api/src/steel-browser-plugin.ts`) - Registers all the necessary services, routes, and hooks - Can be used as a standalone plugin or integrated into your own application - Provides the core functionality of Steel Browser 2. **CDP Service** (`api/src/services/cdp/cdp.service.ts`) - Manages Chrome DevTools Protocol connections - Handles browser lifecycle and page management - Supports plugin system for extensibility 2. **CDP Plugin System** (`api/src/services/cdp/plugins/`) - **BasePlugin**: Abstract base class for all plugins - **PluginManager**: Manages plugin lifecycle and events - Plugins can hook into browser events (launch, page creation, navigation, etc.) 3. **Session Management** (`api/src/services/session.service.ts`) - Manages browser sessions and their state - Handles session persistence and cleanup 4. **File Storage** (`api/src/services/file.service.ts`) - Manages file uploads, downloads, and storage - Supports session-scoped file management ### Using Steel Browser as a Plugin ```typescript import Fastify from 'fastify'; import steelBrowserPlugin, { SteelBrowserConfig } from './api/src/steel-browser-plugin.js'; const fastify = Fastify({ logger: true }); // Register Steel Browser plugin with configuration const config: SteelBrowserConfig = { fileStorage: { maxSizePerSession: 100 * 1024 * 1024, // 100MB }, customWsHandlers: [ // Your custom WebSocket handlers ], }; await fastify.register(steelBrowserPlugin, config); // Your additional routes and plugins await fastify.register(myCustomPlugin); await fastify.listen({ port: 3000 }); ``` ### Configuration Options The `SteelBrowserConfig` interface allows you to customize: - **fileStorage**: Configure file storage limits per session - **customWsHandlers**: Add custom WebSocket handlers for real-time features ### CDP Plugin Development Using the CDP Plugin System, you can create plugins that hook into browser lifecycle events: ```typescript import { BasePlugin, PluginOptions } from './api/src/services/cdp/plugins/core/base-plugin.js'; import { Browser, Page } from 'puppeteer-core'; export class MyCustomPlugin extends BasePlugin { constructor(options: PluginOptions) { super({ name: 'my-custom-plugin', ...options }); } async onBrowserLaunch(browser: Browser): Promise { this.cdpService?.logger.info('Custom plugin: Browser launched'); // Your custom logic here } async onPageCreated(page: Page): Promise { this.cdpService?.logger.info('Custom plugin: New page created'); // Handle new page creation await page.setUserAgent('MyCustomBot/1.0'); } async onPageNavigate(page: Page): Promise { // Handle page navigation const url = page.url(); this.cdpService?.logger.info(`Custom plugin: Navigated to ${url}`); } async onBrowserClose(browser: Browser): Promise { // Cleanup when browser closes this.cdpService?.logger.info('Custom plugin: Browser closed'); } async onShutdown(): Promise { // Cleanup when service shuts down this.cdpService?.logger.info('Custom plugin: Service shutting down'); } } // Register the plugin fastify.cdpService.registerPlugin(new MyCustomPlugin({})); ``` ### Available Plugin Hooks The `BasePlugin` class provides these lifecycle hooks: - `onBrowserLaunch(browser)`: Called when browser instance starts - `onPageCreated(page)`: Called when a new page is created - `onPageNavigate(page)`: Called when a page navigates to a new URL - `onPageUnload(page)`: Called when a page is about to unload - `onBeforePageClose(page)`: Called before a page is closed - `onBrowserClose(browser)`: Called when browser instance closes - `onShutdown()`: Called during service shutdown - `onSessionEnd(sessionConfig)`: Called when a session ends ## 🛠️ Development Workflow ### Branch Naming Convention - `feature/description` - New features - `fix/description` - Bug fixes - `docs/description` - Documentation updates - `refactor/description` - Code refactoring - `test/description` - Test additions/updates ### Commit Message Format We use [Conventional Commits](https://conventionalcommits.org/): ``` type(scope): description [optional body] [optional footer] ``` **Types:** - `feat`: New feature - `fix`: Bug fix - `docs`: Documentation changes - `style`: Code style changes (formatting, etc.) - `refactor`: Code refactoring - `test`: Adding or updating tests - `chore`: Maintenance tasks **Examples:** ``` feat(api): add session timeout configuration fix(ui): resolve session list refresh issue docs: update plugin development guide test(api): add CDP service unit tests ``` ### Code Style & Formatting We use automated formatting and linting: ```bash # Format code (API) npm run pretty -w api # Lint code (UI) npm run lint -w ui # These run automatically on commit via Husky ``` **Style Guidelines:** - Use TypeScript for all new code - Follow existing patterns and conventions - Add JSDoc comments for public APIs - Use descriptive variable and function names - Keep functions small and focused ### Testing > **Note**: We're currently building out our test suite! This is a great area for contributions. ```bash # Tests are currently being set up - for now run these checks: npm run build # Type checking for both API and UI npm run lint -w ui # UI linting npm run pretty -w api # API code formatting # When tests become available: # npm test -w api # npm test -w ui ``` **Testing Guidelines:** - Write unit tests for new functions and classes - Add integration tests for API endpoints - Include end-to-end tests for critical user flows - Mock external dependencies appropriately - Aim for meaningful test coverage, not just high percentages ## 🔄 Pull Request Process ### Before Submitting 1. **Create an Issue** (for non-trivial changes) - Describe the problem or feature request - Discuss the approach with maintainers - Reference the issue in your PR 2. **Test Your Changes** ```bash # Build and test locally npm run build # npm test # tests coming soon # Test with Docker docker-compose -f docker-compose.dev.yml up --build ``` 3. **Update Documentation** - Update relevant README sections - Add/update JSDoc comments - Update API documentation if needed ### PR Checklist - [ ] Branch is up-to-date with main - [ ] Code follows project style guidelines - [ ] Tests pass (when available) - [ ] Documentation is updated - [ ] Commit messages follow conventional format - [ ] PR description clearly explains changes - [ ] Breaking changes are documented ### PR Template ```markdown ## Description Brief description of changes ## Type of Change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Testing - [ ] Tested locally - [ ] Added/updated tests - [ ] Tested with Docker ## Related Issues Fixes #(issue number) ``` ## 🐛 Reporting Issues ### Bug Reports Use our [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) and include: - Clear description of the issue - Steps to reproduce - Expected vs actual behavior - Environment details (OS, Node version, etc.) - Screenshots/logs if applicable ### Feature Requests - Check existing issues first - Describe the use case and motivation - Provide examples of how it would work - Consider implementation complexity ## 🌟 Good First Issues Looking for ways to contribute? Check out issues labeled: - `good first issue` - Perfect for newcomers - `help wanted` - We'd love community help - `documentation` - Improve our docs - `testing` - Help build our test suite ## 📚 Resources ### Learning Resources - [Puppeteer Documentation](https://pptr.dev/) - [Fastify Documentation](https://www.fastify.io/) - [React Documentation](https://react.dev/) - [TypeScript Handbook](https://www.typescriptlang.org/docs/) ### Project Resources - [API Documentation](http://localhost:3000/documentation) - [Steel Cookbook](https://github.com/steel-dev/steel-cookbook) - Usage examples - [Discord Community](https://discord.gg/steel-dev) - Get help and discuss ## 🤝 Community Guidelines ### Code of Conduct We are committed to providing a welcoming and inclusive environment for all contributors. Please: - Be respectful and constructive in discussions - Help newcomers and answer questions - Provide helpful feedback in code reviews - Report any unacceptable behavior to maintainers ### Getting Help - **Discord**: Join our [Discord server](https://discord.gg/steel-dev) for real-time help - **GitHub Issues**: For bug reports and feature requests - **GitHub Discussions**: For questions and general discussion ### Recognition We appreciate all contributions! Contributors are recognized: - In our README contributors section - In our Discord server + Changelog announcements - Through GitHub's contribution tracking - In release notes for significant contributions - Potential invitation to join the core team ## 🔧 Advanced Development ### Environment Variables Key environment variables for development: ```bash # API Configuration NODE_ENV=development HOST=0.0.0.0 PORT=3000 CHROME_HEADLESS=false # For debugging ENABLE_CDP_LOGGING=true # For detailed logs # UI Configuration API_URL=http://localhost:3000 ``` ### Debugging ```bash # Debug API with Chrome DevTools node --inspect ./api/build/index.js # Debug with VS Code # Use the provided launch configurations # Enable verbose logging ENABLE_VERBOSE_LOGGING=true npm run dev -w api ``` ## 📝 Documentation ### Writing Documentation - Use clear, concise language - Include code examples - Add screenshots for UI features - Keep examples up-to-date - Follow markdown best practices ### Documentation Structure - **README.md**: Project overview and quick start - **CONTRIBUTING.md**: This file - contribution guidelines - **API docs**: Auto-generated from OpenAPI schemas - **Architecture docs**: High-level system design - **Plugin docs**: Plugin development guides ## 🚀 Release Process ### Automated Releases We use automated semantic versioning based on conventional commits: - **Automatic Version Bumping**: Versions are automatically bumped based on commit messages - `patch`: Commits with `patch`, `fix`, `fixes`, or `docs` - `minor`: Commits with `feat`, `feature`, or `minor` - `major`: Commits with `breaking`, `breaking-change`, or `major` - **Beta Releases**: All releases are tagged with `-beta` suffix initially - **Automatic Changelog**: Generated from commit history with categorized changes - **GitHub Releases**: Automatically created with changelog and community links ### Versioning We follow [Semantic Versioning](https://semver.org/): - **MAJOR**: Breaking changes (triggered by `breaking`, `breaking-change`, `major` in commits) - **MINOR**: New features, backwards compatible (triggered by `feat`, `feature`, `minor`) - **PATCH**: Bug fixes, backwards compatible (triggered by `patch`, `fix`, `fixes`, `docs`) ### Release Workflow The release process is fully automated via GitHub Actions: 1. **Push to main**: Any push to the main branch triggers the release workflow 2. **Version bump**: Automatically determines version based on commit messages 3. **Changelog generation**: Creates categorized changelog from commits 4. **GitHub release**: Creates release with changelog and community links 5. **Tag creation**: Tags the release with the new version ### Manual Release Steps (if needed) If manual intervention is required: 1. Ensure commit messages follow conventional format 2. Push changes to main branch 3. Monitor the GitHub Actions workflow 4. Verify the release was created successfully 5. Announce on Discord/social media --- ## Thank You! 🙏 Thank you for contributing to Steel Browser! Your contributions help make browser automation more accessible and powerful for developers worldwide. **Happy hacking!** 🎉 ================================================ FILE: Dockerfile ================================================ ARG NODE_VERSION=22.13.0 FROM node:${NODE_VERSION} AS base WORKDIR /app ENV NODE_ENV="production" \ PUPPETEER_CACHE_DIR=/app/.cache \ DISPLAY=:10 \ PATH="/usr/bin:/app/selenium/driver:${PATH}" \ CHROME_BIN=/usr/bin/chromium \ CHROME_PATH=/usr/bin/chromium LABEL org.opencontainers.image.source="https://github.com/steel-dev/steel-browser" # Install dependencies RUN rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade # Stage 1: Build UI FROM node:${NODE_VERSION} AS ui-build WORKDIR /app # Copy root workspace files for UI build COPY --link package.json package-lock.json ./ COPY --link ui/ ./ui/ # Install UI dependencies and build with correct base path RUN npm ci --include=dev -w ui --ignore-scripts RUN VITE_API_URL="" VITE_WS_URL="" npm run build -w ui -- --base=/ui # Stage 2: Build API FROM base AS api-build RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y \ build-essential \ pkg-config \ python-is-python3 \ xvfb # Copy root workspace files for API build COPY --link package.json package-lock.json ./ # Remove or override the prepare script to avoid husky in Docker RUN npm pkg set scripts.prepare="echo skip husky" COPY --link api/ ./api/ # Install dependencies for API RUN npm ci --include=dev --workspace=api # Install dependencies for recorder extension separately RUN cd api/extensions/recorder && npm ci --include=dev && cd - # Build the API package RUN npm run build -w api # Build the recorder extension RUN cd api/extensions/recorder && \ npm run build && \ cd - # Prune dev dependencies RUN npm prune --omit=dev -w api RUN cd api/extensions/recorder && npm prune --omit=dev && cd - # Stage 3: Production FROM base AS production # Install production dependencies RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ wget \ nginx \ gnupg \ fonts-ipafont-gothic \ fonts-wqy-zenhei \ fonts-thai-tlwg \ fonts-kacst \ fonts-freefont-ttf \ libxss1 \ xvfb \ curl \ unzip \ default-jre \ dbus \ dbus-x11 \ procps \ x11-xserver-utils # Install Chrome and ChromeDriver RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ wget \ ca-certificates \ curl \ unzip \ # Download and install Chromium && apt-get install -y chromium chromium-driver \ # Clean up && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && rm -rf /var/cache/apt/* RUN mkdir -p /files # Copy the built API from api-build stage COPY --from=api-build /app /app # Copy the built UI from ui-build stage into the API container COPY --from=ui-build /app/ui/dist /app/ui/dist # Copy entrypoint script COPY --chmod=755 api/entrypoint.sh /app/api/entrypoint.sh EXPOSE 3000 9223 ENV HOST_IP=localhost \ DBUS_SESSION_BUS_ADDRESS=autolaunch: ENTRYPOINT ["/app/api/entrypoint.sh"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================

Steel Logo

Steel

The open-source browser API for AI agents & apps.
The best way to build live web agents and browser automation tools.

[![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) [![License](https://img.shields.io/github/license/steel-dev/steel-browser?color=yellow)](https://github.com/steel-dev/steel-browser/blob/main/LICENSE) [![Discord](https://img.shields.io/discord/1285696350117167226?label=discord)](https://discord.gg/steel-dev) [![Twitter Follow](https://img.shields.io/twitter/follow/steeldotdev)](https://twitter.com/steeldotdev) [![GitHub stars](https://img.shields.io/github/stars/steel-dev/steel-browser)](https://github.com/steel-dev/steel-browser)

Get Started · Documentation · Website · Cookbook

Steel Demo

## ✨ Highlights [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. Under the hood, it manages sessions, pages, and browser processes, allowing you to perform complex browsing tasks programmatically without any of the headaches: - **Full Browser Control**: Uses Puppeteer and CDP for complete control over Chrome instances -- allowing you to connect using Puppeteer, Playwright, or Selenium. - **Session Management**: Maintains browser state, cookies, and local storage across requests - **Proxy Support**: Built-in proxy chain management for IP rotation - **Extension Support**: Load custom Chrome extensions for enhanced functionality - **Debugging Tools**: Built-in request logging and a UI to view/debug sessions with - **Anti-Detection**: Includes stealth plugins and fingerprint management - **Resource Management**: Automatic cleanup and browser lifecycle management - **Browser Tools**: Exposes APIs to quick convert pages to markdown, readability, screenshots, or PDFs. For 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`. > 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. If you love open-source, AI, and dev tools, [we're hiring across the stack](https://jobs.ashbyhq.com/steel)! ### Make sure to give us a star ⭐ Start us on Github! ## 🛠️ Getting Started The 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. ## ⚡ Quick Deploy If you're looking to deploy to a cloud provider, we've got you covered. | Deployment methods | Link | | -------------------- | ----- | | 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) | | 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) | | 1-click deploy to Render | [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy) | ## 💻 Running Locally ### Docker The simplest way to deploy/run a Steel browser instance locally is to run the pre-built Docker image: ```bash # Pull and run the Docker image docker run -p 3000:3000 -p 9223:9223 ghcr.io/steel-dev/steel-browser ``` This 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. You 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. Alternatively, you can run the API and UI separately with docker compose: ```bash docker compose up ``` For Mac Silicon users, you will need to pass this env flag to the Docker compose command to run the images on the correct platform: ```bash DOCKER_DEFAULT_PLATFORM=linux/arm64 docker compose up ``` ## Quickstart for Contributors When 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. ```bash docker compose -f docker-compose.dev.yml up ``` You will also need to run it with `--build` to ensure the Docker images are re-built every time you make changes: ```bash docker compose -f docker-compose.dev.yml up --build ``` If 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. ### Node.js Alternatively, if you have Node.js and Chrome installed, you can run both the server and the UI directly: ```bash npm install npm run dev ``` This will also start the Steel server on port 3000 and the UI on port 5173. Make sure you have the Chrome executable installed and in one of these paths: - **Linux**: `/usr/bin/google-chrome` - **MacOS**: `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` - **Windows**: - `C:\Program Files\Google\Chrome\Application\chrome.exe` OR - `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe` #### Custom Chrome Executable If 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: ```bash export CHROME_EXECUTABLE_PATH=/path/to/your/chrome npm run dev ``` For more details on where this is checked look at [`api/src/utils/browser.ts`](./api/src/utils/browser.ts). ## 🏄🏽‍♂️ Usage > If you're looking for quick examples on how to use Steel, check out the [Cookbook](https://github.com/steel-dev/steel-cookbook). > > Alternatively you can play with the [REPL package](./repl/README.md) too `cd repl` and `npm run start` There are two main ways to interact with the Steel browser API: 1. [Using Sessions](#sessions) 2. [Using the Quick Actions Endpoints](#quick-actions-api) In these examples, we assume your custom Steel API endpoint is `http://localhost:3000`. The 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`. #### Using the SDKs If you prefer to use the our Python and Node SDKs, you can install the `steel-sdk` package for Node or Python. These 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). For 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). ### Sessions The `/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. Once 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: * [Puppeteer Integration](https://docs.steel.dev/overview/guides/puppeteer) * [Playwright with Node](https://docs.steel.dev/overview/guides/playwright-node) * [Playwright with Python](https://docs.steel.dev/overview/guides/playwright-python)
Creating a Session using the Node SDK
```typescript import Steel from 'steel-sdk'; const client = new Steel({ baseURL: "http://localhost:3000", // Custom API Base URL override }); (async () => { try { // Create a new browser session with current API fields const session = await client.sessions.create({ blockAds: true, proxyUrl: "user:pass@host:port", // optional dimensions: { width: 1280, height: 800 }, // optional }); console.log("Created session with ID:", session.id); } catch (error) { console.error("Error creating session:", error); } })(); ````
Creating a Session using the Python SDK
````python import os from steel import Steel client = Steel( base_url="http://localhost:3000", # Custom API Base URL override ) try: # Create a new browser session with custom options session = client.sessions.create( block_ads=True, proxy_url="user:pass@host:port", # optional dimensions={"width": 1280, "height": 800}, # optional ) print("Created session with ID:", session.id) except Exception as e: print("Error creating session:", e) ````
Creating a Session using Curl
```bash # Launch a new browser session curl -X POST http://localhost:3000/v1/sessions \ -H "Content-Type: application/json" \ -d '{ "proxyUrl": "user:pass@host:port", "blockAds": true, "dimensions": { "width": 1280, "height": 800 } }' ```
#### Selenium Sessions >**Note:** This integration does not support all the features of the CDP-based browser sessions API. For 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: ```typescript // Using the Node SDK const session = await client.sessions.create({ isSelenium: true }); ``` ```python # Using the Python SDK session = client.sessions.create(is_selenium=True) ```
Using Curl
```bash # Launch a Selenium session curl -X POST http://localhost:3000/v1/sessions \ -H "Content-Type: application/json" \ -d '{ "isSelenium": true }' ```

The 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).** ### Quick Actions API The `/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:
Scrape a Web Page
Extract the HTML content of a web page. ```bash # Example using the Actions API curl -X POST http://0.0.0.0:3000/v1/scrape \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "delay": 1000 }' ```
Take a Screenshot
Take a screenshot of a web page. ```bash # Example using the Actions API curl -X POST http://0.0.0.0:3000/v1/screenshot \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "fullPage": true }' --output screenshot.png ```
Download a PDF
Download a PDF of a web page. ```bash # Example using the Actions API curl -X POST http://0.0.0.0:3000/v1/pdf \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com" }' --output output.pdf ```
## Get involved Steel browser is an open-source project, and we welcome contributions! - Questions/ideas/feedback? Come hangout on [Discord](https://discord.gg/steel-dev) - Found a bug? Open an issue on [GitHub](https://github.com/steel-dev/steel-browser/issues) ## License [Apache 2.0](./LICENSE) --- Made with ❤️ by the Steel team. ================================================ FILE: api/.dockerignore ================================================ node_modules/ build/ **/.env **/.env.local Dockerfile docker-compose.yml api/extensions/**/dist ================================================ FILE: api/.env.example ================================================ # Server configuration NODE_ENV=development HOST=0.0.0.0 PORT=3000 # Use DOMAIN if you want to specify a full domain name instead of HOST:PORT # DOMAIN=example.com # Set to true to use HTTPS/WSS instead of HTTP/WS USE_SSL=false # Chrome/CDP configuration CHROME_HEADLESS=true CHROME_EXECUTABLE_PATH= CHROME_ARGS= CDP_REDIRECT_PORT=9223 # CDP_DOMAIN=example.com:9223 # Optional proxy configuration PROXY_URL= # Logging LOG_LEVEL=warn # fatal, error, warn, info, debug, trace ENABLE_CDP_LOGGING=false LOG_CUSTOM_EMIT_EVENTS=false ENABLE_VERBOSE_LOGGING=false # Other configuration options SKIP_FINGERPRINT_INJECTION=false DEFAULT_TIMEZONE= DEFAULT_HEADERS= ================================================ FILE: api/.gitattributes ================================================ src/scripts/* linguist-vendored extensions/* linguist-vendored ================================================ FILE: api/.gitignore ================================================ node_modules .pnp .pnp.js coverage .DS_Store *.pem npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* .env .env.local .env.development.local .test.env.local .env.production.local !.env.example production.env .turbo build db/data *.tsbuildinfo out .idea *.env dist .aider* extensions/* !extensions/recorder files/* static/* # Ignore future changes to this file src/services/cdp-lifecycle.service.ts ================================================ FILE: api/.prettierignore ================================================ # Ignore artifacts: build coverage ================================================ FILE: api/.prettierrc ================================================ { "printWidth": 100, "tabWidth": 2, "semi": true, "singleQuote": false, "bracketSpacing": true, "arrowParens": "always" } ================================================ FILE: api/.puppeteerrc.cjs ================================================ const { join } = require("path"); /** * @type {import("puppeteer").Configuration} */ module.exports = { defaultProduct: "chrome", cacheDirectory: join(__dirname, ".cache", "puppeteer"), }; ================================================ FILE: api/Dockerfile ================================================ ARG NODE_VERSION=22.13.0 FROM node:${NODE_VERSION}-slim AS base WORKDIR /app ENV NODE_ENV="production" \ PUPPETEER_CACHE_DIR=/app/.cache \ DISPLAY=:10 \ PATH="/usr/bin:/app/selenium/driver:${PATH}" \ CHROME_BIN=/usr/bin/chromium \ CHROME_PATH=/usr/bin/chromium LABEL org.opencontainers.image.source="https://github.com/steel-dev/steel-browser" # Install dependencies RUN rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \ apt-get update -qq && \ DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \ apt-get install -y --no-install-recommends \ expat \ libxslt1.1 \ libpam0g \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* FROM base AS build RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y \ build-essential \ pkg-config \ python-is-python3 \ xvfb # Copy root workspace files first COPY --link package.json package-lock.json ./ # Remove or override the prepare script to avoid husky in Docker RUN npm pkg set scripts.prepare="echo skip husky" COPY --link api/ ./api/ # Install dependencies for api RUN npm ci --include=dev --workspace=api # Install dependencies for recorder extension separately RUN cd api/extensions/recorder && npm ci --include=dev && cd - # Build the api package RUN npm run build -w api RUN cd api/extensions/recorder && \ npm run build && \ cd - # Prune dev dependencies RUN npm prune --omit=dev -w api RUN cd api/extensions/recorder && npm prune --omit=dev && cd - FROM base AS production # Install dependencies RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ wget \ nginx \ gnupg \ fonts-ipafont-gothic \ fonts-wqy-zenhei \ fonts-thai-tlwg \ fonts-kacst \ fonts-freefont-ttf \ libxss1 \ xvfb \ curl \ unzip \ default-jre \ dbus \ dbus-x11 \ procps \ x11-xserver-utils # Install Chrome and ChromeDriver RUN apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ wget \ ca-certificates \ curl \ unzip \ # Download and install Chromium && apt-get install -y chromium chromium-driver \ # Clean up && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && rm -rf /var/cache/apt/* RUN mkdir -p /files COPY --chmod=755 api/entrypoint.sh /app/api/entrypoint.sh EXPOSE 3000 9223 ENV HOST_IP=localhost \ DBUS_SESSION_BUS_ADDRESS=autolaunch: ENTRYPOINT ["/app/api/entrypoint.sh"] COPY --from=build /app /app ================================================ FILE: api/entrypoint.sh ================================================ #!/bin/sh set -e # Exit on error # Function to log with timestamp log() { if [ "$DEBUG" = "true" ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" fi } # Initialize DBus init_dbus() { log "Initializing DBus..." mkdir -p /var/run/dbus if [ -e /var/run/dbus/pid ]; then rm -f /var/run/dbus/pid fi dbus-daemon --system --fork sleep 2 # Give DBus time to initialize if dbus-send --system --print-reply --dest=org.freedesktop.DBus \ /org/freedesktop/DBus org.freedesktop.DBus.ListNames >/dev/null 2>&1; then log "DBus initialized successfully" return 0 else log "ERROR: DBus failed to initialize" return 1 fi } # Verify Chrome and ChromeDriver installation verify_chrome() { log "Verifying Chrome installation..." # Check Chrome binary and version if [ ! -f "/usr/bin/chromium" ] && [ -z "$CHROME_EXECUTABLE_PATH" ]; then log "ERROR: Chrome binary not found at /usr/bin/chromium and CHROME_EXECUTABLE_PATH not set" return 1 fi if [ -f "/usr/bin/chromium" ]; then chrome_version=$(chromium --version 2>/dev/null || echo "unknown") elif [ -n "$CHROME_EXECUTABLE_PATH" ] && [ -f "$CHROME_EXECUTABLE_PATH" ]; then chrome_version=$("$CHROME_EXECUTABLE_PATH" --version 2>/dev/null || echo "unknown") else chrome_version="unknown" fi log "Chrome version: $chrome_version" # Check ChromeDriver binary and version if [ ! -f "/usr/bin/chromedriver" ]; then log "ERROR: ChromeDriver not found at /usr/bin/chromedriver" return 1 fi chromedriver_version=$(chromedriver --version 2>/dev/null || echo "unknown") log "ChromeDriver version: $chromedriver_version" log "Chrome environment configured successfully" return 0 } # Start nginx with better error handling start_nginx() { if [ "$START_NGINX" = "true" ]; then log "Starting nginx..." nginx -c /app/api/nginx.conf # Wait for nginx to start max_attempts=10 attempt=1 while [ $attempt -le $max_attempts ]; do if nginx -t >/dev/null 2>&1; then log "Nginx started successfully" return 0 fi log "Attempt $attempt/$max_attempts: Waiting for nginx..." attempt=$((attempt + 1)) sleep 1 done log "ERROR: Nginx failed to start properly" return 1 else log "Skipping nginx startup (--no-nginx flag detected)" return 0 fi } # Main execution main() { # Parse arguments START_NGINX=true for arg in "$@"; do if [ "$arg" = "--no-nginx" ]; then START_NGINX=false break fi done if [ "$DEBUG" = "true" ]; then init_dbus || exit 1 verify_chrome || exit 1 fi start_nginx || exit 1 # Set required environment variables export CDP_REDIRECT_PORT=9223 export DISPLAY=:10 # Log environment state log "Environment configuration:" log "HOST=$HOST" log "CDP_REDIRECT_PORT=$CDP_REDIRECT_PORT" log "NODE_ENV=$NODE_ENV" # Start the application # Run the `npm run start` command but without npm. # NPM will introduce its own signal handling # which will prevent the container from waiting # for a session to be released before stopping gracefully log "Starting Steel Browser API..." exec node ./api/build/index.js } main "$@" ================================================ FILE: api/extensions/recorder/.gitignore ================================================ node_modules/ dist/ ================================================ FILE: api/extensions/recorder/manifest.json ================================================ { "manifest_version": 3, "name": "Steel Recording Extension", "version": "1.0", "permissions": [ "scripting", "activeTab" ], "host_permissions": [ "http://localhost:3000/*", "http://0.0.0.0:3000/*" ], "background": { "service_worker": "dist/background.js" }, "content_scripts": [ { "matches": [ "" ], "js": [ "dist/inject.js" ] } ] } ================================================ FILE: api/extensions/recorder/package.json ================================================ { "name": "steel-recording-extension", "version": "1.0.0", "private": true, "type": "module", "dependencies": { "rrweb": "^2.0.0-alpha.4", "@rrweb/packer": "^2.0.0-alpha.4" }, "scripts": { "build": "webpack" }, "devDependencies": { "webpack": "^5.89.0", "webpack-cli": "^5.1.4" } } ================================================ FILE: api/extensions/recorder/src/background.js ================================================ const LOCAL_API_URL = "http://localhost:3000/v1/events"; const FALLBACK_API_URL = "http://0.0.0.0:3000/v1/events"; // Need to point to 0.0.0.0 in some deploys let currentApiUrl = LOCAL_API_URL; async function injectScript(tabId, changeInfo, tab) { if (changeInfo.status === "complete" && tab.url) { try { await chrome.scripting.executeScript({ target: { tabId }, files: ["inject.js"], }); } catch (error) { console.error("Script injection failed:", error); } } } // Listen for tab updates chrome.tabs.onUpdated.addListener(injectScript); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type !== "SAVE_EVENTS") { return false; } console.log("[Recorder Background] Saving events to", currentApiUrl); const sendEvents = async (url) => { try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(message), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } sendResponse({ success: true }); } catch (error) { if (url === LOCAL_API_URL) { // Retry with fallback URL currentApiUrl = FALLBACK_API_URL; return sendEvents(FALLBACK_API_URL); } sendResponse({ success: false, error: error.message }); } }; sendEvents(currentApiUrl); return true; }); ================================================ FILE: api/extensions/recorder/src/inject.js ================================================ import { record } from "rrweb"; import { pack } from "@rrweb/packer"; record({ emit: (event) => { chrome.runtime.sendMessage( { type: "SAVE_EVENTS", events: [event], }, (response) => { if (!response.success) { console.error("[Recorder] Failed to save events:", response.error); } }, ); }, packFn: pack, sampling: { media: 800, }, inlineImages: true, collectFonts: true, recordCrossOriginIframes: true, recordCanvas: true, }); const enableWebRtcSites = ["meet.google.com", "zoom.us", "discord.com"]; try { const hostname = new URL(window.location.href).hostname; const shouldDisableWebRtc = !enableWebRtcSites.includes(hostname); if (shouldDisableWebRtc) { navigator.mediaDevices.getUserMedia = navigator.webkitGetUserMedia = navigator.mozGetUserMedia = navigator.getUserMedia = webkitRTCPeerConnection = RTCPeerConnection = MediaStreamTrack = undefined; Object.defineProperty(window, "RTCPeerConnection", { get: () => { return {}; }, }); Object.defineProperty(window, "RTCDataChannel", { get: () => { return {}; }, }); } } catch (e) { console.error(`Error processing URL for WebRTC blocking: ${e}`); } ================================================ FILE: api/extensions/recorder/webpack.config.mjs ================================================ import path from "path"; import { fileURLToPath } from "url"; import { dirname } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export default { mode: "production", entry: { inject: path.resolve(__dirname, "src/inject.js"), background: path.resolve(__dirname, "src/background.js"), }, output: { filename: "[name].js", path: path.resolve(__dirname, "dist"), }, optimization: { minimize: false, }, }; ================================================ FILE: api/nginx.conf ================================================ events { worker_connections 1024; } http { map $http_upgrade $connection_upgrade { default upgrade; '' keep-alive; } server { listen 9223; location / { proxy_pass http://127.0.0.1:9222; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; gzip off; proxy_set_header Accept-Encoding ""; proxy_read_timeout 86400; proxy_send_timeout 86400; proxy_buffering off; proxy_request_buffering off; chunked_transfer_encoding on; } } } ================================================ FILE: api/openapi/generate.ts ================================================ import { writeFileSync } from "fs"; import { server } from "../src"; import { env } from "../src/env.js"; interface OpenAPIServer { url: string; description?: string; } interface OpenAPIDocument { servers?: OpenAPIServer[]; [key: string]: any; } server.ready(() => { let openApiJSON = server.swagger() as OpenAPIDocument; // Add server URL from environment variables. const serverUrl = `http://${env.HOST}:${env.PORT}`; if (!openApiJSON.servers) { openApiJSON.servers = []; } openApiJSON.servers.push({ url: serverUrl, description: "Local server from env variables", }); writeFileSync("./openapi/schemas.json", JSON.stringify(openApiJSON, null, 2), "utf-8"); console.log("OpenAPI JSON has been written to schemas.json"); server.close(() => { console.log("Server closed after generating schemas."); process.exit(0); }); }); ================================================ FILE: api/openapi/schemas.json ================================================ { "openapi": "3.0.3", "info": { "title": "Steel Browser Instance API", "description": "Documentation for controlling a single instance of Steel Browser", "version": "0.0.1" }, "components": { "securitySchemes": {}, "schemas": { "ScrapeRequest": { "title": "ScrapeRequest", "type": "object", "properties": { "url": { "type": "string" }, "format": { "type": "array", "items": { "type": "string", "enum": [ "html", "readability", "cleaned_html", "markdown" ] } }, "screenshot": { "type": "boolean" }, "pdf": { "type": "boolean" }, "proxyUrl": { "type": "string", "nullable": true, "description": "Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used." }, "delay": { "type": "number" }, "logUrl": { "type": "string" } }, "additionalProperties": false }, "ScrapeResponse": { "title": "ScrapeResponse", "type": "object", "properties": { "content": { "type": "object", "additionalProperties": {} }, "metadata": { "type": "object", "properties": { "title": { "type": "string" }, "language": { "type": "string" }, "urlSource": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" }, "description": { "type": "string" }, "keywords": { "type": "string" }, "author": { "type": "string" }, "ogTitle": { "type": "string" }, "ogDescription": { "type": "string" }, "ogImage": { "type": "string" }, "ogUrl": { "type": "string" }, "ogSiteName": { "type": "string" }, "articleAuthor": { "type": "string" }, "publishedTime": { "type": "string" }, "modifiedTime": { "type": "string" }, "canonical": { "type": "string" }, "favicon": { "type": "string" }, "jsonLd": {}, "statusCode": { "type": "integer" } }, "required": [ "statusCode" ], "additionalProperties": false }, "links": { "type": "array", "items": { "type": "object", "properties": { "url": { "type": "string" }, "text": { "type": "string" } }, "required": [ "url", "text" ], "additionalProperties": false } }, "screenshot": { "type": "string" }, "pdf": { "type": "string" } }, "required": [ "content", "metadata", "links" ], "additionalProperties": false }, "ScreenshotRequest": { "title": "ScreenshotRequest", "type": "object", "properties": { "url": { "type": "string" }, "proxyUrl": { "type": "string", "nullable": true, "description": "Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used." }, "delay": { "type": "number" }, "fullPage": { "type": "boolean" }, "logUrl": { "type": "string" } }, "additionalProperties": false }, "ScreenshotResponse": { "title": "ScreenshotResponse" }, "PDFRequest": { "title": "PDFRequest", "type": "object", "properties": { "url": { "type": "string" }, "proxyUrl": { "type": "string", "nullable": true, "description": "Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used." }, "delay": { "type": "number" }, "logUrl": { "type": "string" } }, "additionalProperties": false }, "PDFResponse": { "title": "PDFResponse" }, "CreateSession": { "title": "CreateSession", "type": "object", "properties": { "sessionId": { "type": "string", "format": "uuid", "description": "Unique identifier for the session" }, "proxyUrl": { "type": "string", "description": "Proxy URL to use for the session" }, "userAgent": { "type": "string", "description": "User agent string to use for the session" }, "sessionContext": { "type": "object", "properties": { "cookies": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the cookie" }, "value": { "type": "string", "description": "The value of the cookie" }, "url": { "type": "string", "description": "The URL of the cookie" }, "domain": { "type": "string", "description": "The domain of the cookie" }, "path": { "type": "string", "description": "The path of the cookie" }, "secure": { "type": "boolean", "description": "Whether the cookie is secure" }, "httpOnly": { "type": "boolean", "description": "Whether the cookie is HTTP only" }, "sameSite": { "type": "string", "enum": [ "Strict", "Lax", "None" ], "description": "The same site attribute of the cookie" }, "size": { "type": "number", "description": "The size of the cookie" }, "expires": { "type": "number", "description": "The expiration date of the cookie" }, "partitionKey": { "type": "object", "properties": { "topLevelSite": { "type": "string", "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." }, "hasCrossSiteAncestor": { "type": "boolean", "description": "Indicates if the cookie has any ancestors that are cross-site to the topLevelSite." } }, "required": [ "topLevelSite", "hasCrossSiteAncestor" ], "additionalProperties": false, "description": "The partition key of the cookie" }, "session": { "type": "boolean", "description": "Whether the cookie is a session cookie" }, "priority": { "type": "string", "enum": [ "Low", "Medium", "High" ], "description": "The priority of the cookie" }, "sameParty": { "type": "boolean", "description": "Whether the cookie is a same party cookie" }, "sourceScheme": { "type": "string", "enum": [ "Unset", "NonSecure", "Secure" ], "description": "The source scheme of the cookie" }, "sourcePort": { "type": "number", "description": "The source port of the cookie" } }, "required": [ "name", "value" ], "additionalProperties": false }, "description": "Cookies to initialize in the session" }, "localStorage": { "type": "object", "additionalProperties": { "type": "object", "additionalProperties": { "type": "string" } }, "description": "Domain-specific localStorage items to initialize in the session" }, "sessionStorage": { "type": "object", "additionalProperties": { "type": "object", "additionalProperties": { "type": "string" } }, "description": "Domain-specific sessionStorage items to initialize in the session" }, "indexedDB": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "records": { "type": "array", "items": { "type": "object", "properties": { "key": {}, "value": {}, "blobFiles": { "type": "array", "items": { "type": "object", "properties": { "blobNumber": { "type": "number" }, "mimeType": { "type": "string" }, "size": { "type": "number" }, "filename": { "type": "string" }, "lastModified": { "type": "string", "format": "date-time" }, "path": { "type": "string" } }, "required": [ "blobNumber", "mimeType", "size" ], "additionalProperties": false } } }, "additionalProperties": false } } }, "required": [ "id", "name", "records" ], "additionalProperties": false } } }, "required": [ "id", "name", "data" ], "additionalProperties": false } }, "description": "Domain-specific indexedDB items to initialize in the session" } }, "additionalProperties": false, "description": "Session context data to be used in the created session" }, "isSelenium": { "type": "boolean", "description": "Indicates if Selenium is used in the session" }, "blockAds": { "type": "boolean", "description": "Flag to indicate if ads should be blocked in the session" }, "optimizeBandwidth": { "anyOf": [ { "type": "boolean" }, { "type": "object", "properties": { "blockImages": { "type": "boolean" }, "blockMedia": { "type": "boolean" }, "blockStylesheets": { "type": "boolean" }, "blockHosts": { "type": "array", "items": { "type": "string" } }, "blockUrlPatterns": { "type": "array", "items": { "type": "string" } } }, "additionalProperties": false } ], "description": "Enable bandwidth optimizations. Passing true enables all flags (except hosts/patterns). Object allows granular control." }, "skipFingerprintInjection": { "type": "boolean", "description": "Flag to indicate if fingerprint injection should be skipped for this session." }, "deviceConfig": { "type": "object", "properties": { "device": { "type": "string", "enum": [ "desktop", "mobile" ], "default": "desktop" } }, "additionalProperties": false, "description": "Device configuration for the session. Specify 'mobile' for mobile device fingerprints and configurations." }, "logSinkUrl": { "type": "string", "description": "Deprecated: Log sink URL to use for the session" }, "extensions": { "type": "array", "items": { "type": "string" }, "description": "Extensions to use for the session" }, "persist": { "type": "boolean", "description": "Flag to indicate if session should be persisted" }, "userDataDir": { "type": "string", "description": "User data directory path to use for the session" }, "timezone": { "type": "string", "description": "Timezone to use for the session" }, "dimensions": { "type": "object", "properties": { "width": { "type": "number" }, "height": { "type": "number" } }, "required": [ "width", "height" ], "additionalProperties": false, "description": "Dimensions to use for the session" }, "userPreferences": { "type": "object", "additionalProperties": {}, "description": "Chrome user preferences to customize browser behavior (e.g., font size, popup blocking, notification settings)" }, "extra": { "type": "object", "additionalProperties": {}, "description": "Extra metadata to help initialize the session" }, "credentials": { "type": "object", "properties": { "autoSubmit": { "anyOf": [ { "type": "boolean" }, { "not": {} } ] }, "blurFields": { "anyOf": [ { "type": "boolean" }, { "not": {} } ] }, "exactOrigin": { "anyOf": [ { "type": "boolean" }, { "not": {} } ] } }, "additionalProperties": false, "description": "Configuration for session credentials" }, "headless": { "type": "boolean", "description": "Headless mode for the session" } }, "additionalProperties": false }, "SessionDetails": { "title": "SessionDetails", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the session" }, "createdAt": { "type": "string", "format": "date-time", "description": "Timestamp when the session started" }, "status": { "type": "string", "enum": [ "idle", "live", "released", "failed" ], "description": "Status of the session" }, "duration": { "type": "integer", "description": "Duration of the session in milliseconds" }, "eventCount": { "type": "integer", "description": "Number of events processed in the session" }, "dimensions": { "type": "object", "properties": { "width": { "type": "number" }, "height": { "type": "number" } }, "required": [ "width", "height" ], "additionalProperties": false, "description": "Dimensions used for the session" }, "timeout": { "type": "integer", "description": "Session timeout duration in milliseconds" }, "creditsUsed": { "type": "integer", "description": "Amount of credits consumed by the session" }, "websocketUrl": { "type": "string", "description": "URL for the session's WebSocket connection" }, "debugUrl": { "type": "string", "description": "URL for a viewing the live browser instance for the session" }, "debuggerUrl": { "type": "string", "description": "URL for debugging the session" }, "sessionViewerUrl": { "type": "string", "description": "URL to view session details" }, "userAgent": { "type": "string", "description": "User agent string used in the session" }, "proxy": { "type": "string", "description": "Proxy server used for the session" }, "proxyTxBytes": { "type": "integer", "minimum": 0, "description": "Amount of data transmitted through the proxy" }, "proxyRxBytes": { "type": "integer", "minimum": 0, "description": "Amount of data received through the proxy" }, "solveCaptcha": { "type": "boolean", "description": "Indicates if captcha solving is enabled" }, "isSelenium": { "type": "boolean", "description": "Indicates if Selenium is used in the session" } }, "required": [ "id", "createdAt", "status", "duration", "eventCount", "timeout", "creditsUsed", "websocketUrl", "debugUrl", "debuggerUrl", "sessionViewerUrl", "proxyTxBytes", "proxyRxBytes" ], "additionalProperties": false }, "MultipleSessions": { "title": "MultipleSessions", "type": "object", "properties": { "sessions": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the session" }, "createdAt": { "type": "string", "format": "date-time", "description": "Timestamp when the session started" }, "status": { "type": "string", "enum": [ "idle", "live", "released", "failed" ], "description": "Status of the session" }, "duration": { "type": "integer", "description": "Duration of the session in milliseconds" }, "eventCount": { "type": "integer", "description": "Number of events processed in the session" }, "dimensions": { "type": "object", "properties": { "width": { "type": "number" }, "height": { "type": "number" } }, "required": [ "width", "height" ], "additionalProperties": false, "description": "Dimensions used for the session" }, "timeout": { "type": "integer", "description": "Session timeout duration in milliseconds" }, "creditsUsed": { "type": "integer", "description": "Amount of credits consumed by the session" }, "websocketUrl": { "type": "string", "description": "URL for the session's WebSocket connection" }, "debugUrl": { "type": "string", "description": "URL for a viewing the live browser instance for the session" }, "debuggerUrl": { "type": "string", "description": "URL for debugging the session" }, "sessionViewerUrl": { "type": "string", "description": "URL to view session details" }, "userAgent": { "type": "string", "description": "User agent string used in the session" }, "proxy": { "type": "string", "description": "Proxy server used for the session" }, "proxyTxBytes": { "type": "integer", "minimum": 0, "description": "Amount of data transmitted through the proxy" }, "proxyRxBytes": { "type": "integer", "minimum": 0, "description": "Amount of data received through the proxy" }, "solveCaptcha": { "type": "boolean", "description": "Indicates if captcha solving is enabled" }, "isSelenium": { "type": "boolean", "description": "Indicates if Selenium is used in the session" } }, "required": [ "id", "createdAt", "status", "duration", "eventCount", "timeout", "creditsUsed", "websocketUrl", "debugUrl", "debuggerUrl", "sessionViewerUrl", "proxyTxBytes", "proxyRxBytes" ], "additionalProperties": false } } }, "required": [ "sessions" ], "additionalProperties": false }, "SessionContextSchema": { "title": "SessionContextSchema", "type": "object", "properties": { "cookies": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the cookie" }, "value": { "type": "string", "description": "The value of the cookie" }, "url": { "type": "string", "description": "The URL of the cookie" }, "domain": { "type": "string", "description": "The domain of the cookie" }, "path": { "type": "string", "description": "The path of the cookie" }, "secure": { "type": "boolean", "description": "Whether the cookie is secure" }, "httpOnly": { "type": "boolean", "description": "Whether the cookie is HTTP only" }, "sameSite": { "type": "string", "enum": [ "Strict", "Lax", "None" ], "description": "The same site attribute of the cookie" }, "size": { "type": "number", "description": "The size of the cookie" }, "expires": { "type": "number", "description": "The expiration date of the cookie" }, "partitionKey": { "type": "object", "properties": { "topLevelSite": { "type": "string", "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." }, "hasCrossSiteAncestor": { "type": "boolean", "description": "Indicates if the cookie has any ancestors that are cross-site to the topLevelSite." } }, "required": [ "topLevelSite", "hasCrossSiteAncestor" ], "additionalProperties": false, "description": "The partition key of the cookie" }, "session": { "type": "boolean", "description": "Whether the cookie is a session cookie" }, "priority": { "type": "string", "enum": [ "Low", "Medium", "High" ], "description": "The priority of the cookie" }, "sameParty": { "type": "boolean", "description": "Whether the cookie is a same party cookie" }, "sourceScheme": { "type": "string", "enum": [ "Unset", "NonSecure", "Secure" ], "description": "The source scheme of the cookie" }, "sourcePort": { "type": "number", "description": "The source port of the cookie" } }, "required": [ "name", "value" ], "additionalProperties": false }, "description": "Cookies to initialize in the session" }, "localStorage": { "type": "object", "additionalProperties": { "type": "object", "additionalProperties": { "type": "string" } }, "description": "Domain-specific localStorage items to initialize in the session" }, "sessionStorage": { "type": "object", "additionalProperties": { "type": "object", "additionalProperties": { "type": "string" } }, "description": "Domain-specific sessionStorage items to initialize in the session" }, "indexedDB": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" }, "records": { "type": "array", "items": { "type": "object", "properties": { "key": {}, "value": {}, "blobFiles": { "type": "array", "items": { "type": "object", "properties": { "blobNumber": { "type": "number" }, "mimeType": { "type": "string" }, "size": { "type": "number" }, "filename": { "type": "string" }, "lastModified": { "type": "string", "format": "date-time" }, "path": { "type": "string" } }, "required": [ "blobNumber", "mimeType", "size" ], "additionalProperties": false } } }, "additionalProperties": false } } }, "required": [ "id", "name", "records" ], "additionalProperties": false } } }, "required": [ "id", "name", "data" ], "additionalProperties": false } }, "description": "Domain-specific indexedDB items to initialize in the session" } }, "additionalProperties": false }, "RecordedEvents": { "title": "RecordedEvents", "type": "object", "properties": { "events": { "type": "array", "description": "Events to emit" } }, "required": [ "events" ], "additionalProperties": false }, "ReleaseSession": { "title": "ReleaseSession", "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the session" }, "createdAt": { "type": "string", "format": "date-time", "description": "Timestamp when the session started" }, "status": { "type": "string", "enum": [ "idle", "live", "released", "failed" ], "description": "Status of the session" }, "duration": { "type": "integer", "description": "Duration of the session in milliseconds" }, "eventCount": { "type": "integer", "description": "Number of events processed in the session" }, "dimensions": { "type": "object", "properties": { "width": { "type": "number" }, "height": { "type": "number" } }, "required": [ "width", "height" ], "additionalProperties": false, "description": "Dimensions used for the session" }, "timeout": { "type": "integer", "description": "Session timeout duration in milliseconds" }, "creditsUsed": { "type": "integer", "description": "Amount of credits consumed by the session" }, "websocketUrl": { "type": "string", "description": "URL for the session's WebSocket connection" }, "debugUrl": { "type": "string", "description": "URL for a viewing the live browser instance for the session" }, "debuggerUrl": { "type": "string", "description": "URL for debugging the session" }, "sessionViewerUrl": { "type": "string", "description": "URL to view session details" }, "userAgent": { "type": "string", "description": "User agent string used in the session" }, "proxy": { "type": "string", "description": "Proxy server used for the session" }, "proxyTxBytes": { "type": "integer", "minimum": 0, "description": "Amount of data transmitted through the proxy" }, "proxyRxBytes": { "type": "integer", "minimum": 0, "description": "Amount of data received through the proxy" }, "solveCaptcha": { "type": "boolean", "description": "Indicates if captcha solving is enabled" }, "isSelenium": { "type": "boolean", "description": "Indicates if Selenium is used in the session" }, "success": { "type": "boolean", "description": "Indicates if the session was successfully released" } }, "required": [ "id", "createdAt", "status", "duration", "eventCount", "timeout", "creditsUsed", "websocketUrl", "debugUrl", "debuggerUrl", "sessionViewerUrl", "proxyTxBytes", "proxyRxBytes", "success" ], "additionalProperties": false }, "SessionStreamQuery": { "title": "SessionStreamQuery", "type": "object", "properties": { "showControls": { "type": "boolean", "default": true, "description": "Show controls in the browser iframe" }, "theme": { "type": "string", "enum": [ "dark", "light" ], "default": "dark", "description": "Theme of the browser iframe" }, "interactive": { "type": "boolean", "default": true, "description": "Make the browser iframe interactive" }, "pageId": { "type": "string", "description": "Page ID to connect to" }, "pageIndex": { "type": "string", "description": "Page index (or tab index) to connect to" } }, "additionalProperties": false }, "SessionStreamResponse": { "title": "SessionStreamResponse", "type": "string", "description": "HTML content for the session streamer view" }, "SessionLiveDetailsResponse": { "title": "SessionLiveDetailsResponse", "type": "object", "properties": { "sessionViewerUrl": { "type": "string" }, "sessionViewerFullscreenUrl": { "type": "string" }, "websocketUrl": { "type": "string" }, "pages": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "title": { "type": "string" }, "favicon": { "type": "string", "nullable": true } }, "required": [ "id", "url", "title", "favicon" ], "additionalProperties": false } }, "browserState": { "type": "object", "properties": { "status": { "type": "string", "enum": [ "idle", "live", "released", "failed" ] }, "userAgent": { "type": "string" }, "browserVersion": { "type": "string" }, "initialDimensions": { "type": "object", "properties": { "width": { "type": "number" }, "height": { "type": "number" } }, "required": [ "width", "height" ], "additionalProperties": false }, "pageCount": { "type": "number" } }, "required": [ "status", "userAgent", "browserVersion", "initialDimensions", "pageCount" ], "additionalProperties": false } }, "required": [ "sessionViewerUrl", "sessionViewerFullscreenUrl", "websocketUrl", "pages", "browserState" ], "additionalProperties": false }, "LogQuerySchema": { "title": "LogQuerySchema", "type": "object", "properties": { "startTime": { "type": "string", "format": "date-time" }, "endTime": { "type": "string", "format": "date-time" }, "eventTypes": { "type": "string" }, "pageId": { "type": "string" }, "targetType": { "type": "string" }, "limit": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100 }, "offset": { "type": "integer", "minimum": 0, "default": 0 } }, "additionalProperties": false }, "LogStatsSchema": { "title": "LogStatsSchema", "type": "object", "properties": { "totalEvents": { "type": "number" }, "oldestEvent": { "type": "string", "format": "date-time", "nullable": true }, "newestEvent": { "type": "string", "format": "date-time", "nullable": true }, "sizeBytes": { "type": "number" } }, "required": [ "totalEvents", "oldestEvent", "newestEvent", "sizeBytes" ], "additionalProperties": false }, "LogQueryResultSchema": { "title": "LogQueryResultSchema", "type": "object", "properties": { "events": { "type": "array", "items": { "type": "object", "additionalProperties": {} } }, "total": { "type": "number" }, "hasMore": { "type": "boolean" } }, "required": [ "events", "total", "hasMore" ], "additionalProperties": false }, "ExportLogsSchema": { "title": "ExportLogsSchema", "type": "object", "properties": { "query": { "type": "object", "properties": { "startTime": { "type": "string", "format": "date-time" }, "endTime": { "type": "string", "format": "date-time" }, "eventTypes": { "type": "string" }, "pageId": { "type": "string" }, "targetType": { "type": "string" }, "limit": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100 }, "offset": { "type": "integer", "minimum": 0, "default": 0 } }, "additionalProperties": false } }, "additionalProperties": false }, "GetDevtoolsUrlSchema": { "title": "GetDevtoolsUrlSchema", "type": "object", "properties": { "pageId": { "type": "string" } }, "additionalProperties": false }, "LaunchRequest": { "title": "LaunchRequest", "type": "object", "properties": { "options": { "type": "object", "properties": { "args": { "type": "array", "items": { "type": "string" } }, "chromiumSandbox": { "type": "boolean" }, "devtools": { "type": "boolean" }, "downloadsPath": { "type": "string" }, "headless": { "type": "boolean" }, "ignoreDefaultArgs": { "anyOf": [ { "type": "boolean" }, { "type": "array", "items": { "type": "string" } } ] }, "proxyUrl": { "type": "string" }, "timeout": { "type": "number" }, "tracesDir": { "type": "string" } }, "additionalProperties": false }, "req": {}, "stealth": { "type": "boolean" }, "cookies": { "type": "array" }, "userAgent": { "type": "string" }, "extensions": { "type": "array", "items": { "type": "string" } }, "logSinkUrl": { "type": "string", "description": "Deprecated" }, "customHeaders": { "type": "object", "additionalProperties": { "type": "string" } }, "timezone": { "type": "string" }, "dimensions": { "type": "object", "properties": { "width": { "type": "number" }, "height": { "type": "number" } }, "required": [ "width", "height" ], "additionalProperties": false, "nullable": true } }, "required": [ "options" ], "additionalProperties": false }, "LaunchResponse": { "title": "LaunchResponse", "type": "object", "properties": { "success": { "type": "boolean" } }, "required": [ "success" ], "additionalProperties": false }, "FileUploadRequest": { "title": "FileUploadRequest", "type": "object", "properties": { "file": { "description": "The file to upload (binary) or URL string to download from" }, "path": { "type": "string", "description": "Path to the file in the storage system" } }, "additionalProperties": false }, "FileDetails": { "title": "FileDetails", "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file in the storage system" }, "size": { "type": "number", "description": "Size of the file in bytes" }, "lastModified": { "type": "string", "format": "date-time", "description": "Timestamp when the file was last updated" } }, "required": [ "path", "size", "lastModified" ], "additionalProperties": false }, "MultipleFiles": { "title": "MultipleFiles", "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file in the storage system" }, "size": { "type": "number", "description": "Size of the file in bytes" }, "lastModified": { "type": "string", "format": "date-time", "description": "Timestamp when the file was last updated" } }, "required": [ "path", "size", "lastModified" ], "additionalProperties": false }, "description": "Array of files for the current page" } }, "required": [ "data" ], "additionalProperties": false } } }, "paths": { "/v1/scrape": { "post": { "operationId": "scrape", "summary": "Scrape a URL", "tags": [ "Browser Actions" ], "description": "Scrape a URL", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScrapeRequest" } } } }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScrapeResponse" } } } } } } }, "/v1/screenshot": { "post": { "operationId": "screenshot", "summary": "Take a screenshot", "tags": [ "Browser Actions" ], "description": "Take a screenshot", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScreenshotRequest" } } } }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScreenshotResponse" } } } } } } }, "/v1/pdf": { "post": { "operationId": "pdf", "summary": "Get the PDF content of a page", "tags": [ "Browser Actions" ], "description": "Get the PDF content of a page", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PDFRequest" } } } }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PDFResponse" } } } } } } }, "/v1/health": { "get": { "operationId": "health", "summary": "Check if the server and browser are running", "tags": [ "Health" ], "description": "Check if the server and browser are running", "responses": { "200": { "description": "Default Response" } } } }, "/v1/sessions": { "post": { "operationId": "launch_browser_session", "summary": "Launch a browser session", "tags": [ "Sessions" ], "description": "Launch a browser session", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateSession" } } } }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionDetails" } } } } } }, "get": { "operationId": "get_sessions", "summary": "Get all sessions", "tags": [ "Sessions" ], "description": "Get all sessions", "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MultipleSessions" } } } } } } }, "/v1/sessions/{sessionId}": { "get": { "operationId": "get_session_details", "summary": "Get session details", "tags": [ "Sessions" ], "description": "Get session details", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true } ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionDetails" } } } } } } }, "/v1/sessions/{sessionId}/context": { "get": { "operationId": "get_browser_context", "summary": "Get a browser context", "tags": [ "Sessions" ], "description": "Get a browser context", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true } ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionContextSchema" } } } } } } }, "/v1/sessions/{sessionId}/release": { "post": { "operationId": "release_browser_session", "summary": "Release a browser session", "tags": [ "Sessions" ], "description": "Release a browser session", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true } ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReleaseSession" } } } } } } }, "/v1/sessions/release": { "post": { "operationId": "release_browser_sessions", "summary": "Release browser sessions", "tags": [ "Sessions" ], "description": "Release browser sessions", "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReleaseSession" } } } } } } }, "/v1/sessions/debug": { "get": { "operationId": "get_session_debugger_stream", "summary": "Get session debugger view", "tags": [ "Sessions" ], "description": "Returns an HTML page with a live debugger view of the session", "parameters": [ { "schema": { "type": "boolean", "default": true }, "in": "query", "name": "showControls", "required": false, "description": "Show controls in the browser iframe" }, { "schema": { "type": "string", "enum": [ "dark", "light" ], "default": "dark" }, "in": "query", "name": "theme", "required": false, "description": "Theme of the browser iframe" }, { "schema": { "type": "boolean", "default": true }, "in": "query", "name": "interactive", "required": false, "description": "Make the browser iframe interactive" }, { "schema": { "type": "string" }, "in": "query", "name": "pageId", "required": false, "description": "Page ID to connect to" }, { "schema": { "type": "string" }, "in": "query", "name": "pageIndex", "required": false, "description": "Page index (or tab index) to connect to" } ], "responses": { "200": { "description": "HTML content for the session streamer view", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionStreamResponse" } } } } } } }, "/v1/events": { "post": { "operationId": "receive_events", "summary": "Receive recorded events from the browser", "tags": [ "Sessions" ], "description": "Receive recorded events from the browser", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RecordedEvents" } } } }, "responses": { "200": { "description": "Default Response" } } } }, "/v1/sessions/{id}/live-details": { "get": { "operationId": "get_session_live_details", "summary": "Get session live details", "tags": [ "Sessions" ], "description": "Returns the live state of the session, including pages, tabs, and browser state", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "id", "required": true } ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionLiveDetailsResponse" } } } } } } }, "/v1/sessions/scrape": { "post": { "operationId": "scrape_session", "summary": "Scrape Current Session", "tags": [ "Sessions" ], "description": "Scrape Current Session", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScrapeRequest" } } } }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScrapeResponse" } } } } } } }, "/v1/sessions/screenshot": { "post": { "operationId": "screenshot_session", "summary": "Take Screenshot of Current Session", "tags": [ "Sessions" ], "description": "Take Screenshot of Current Session", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScreenshotRequest" } } } }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScreenshotResponse" } } } } } } }, "/v1/sessions/pdf": { "post": { "operationId": "pdf_session", "summary": "Generate PDF of Current Session", "tags": [ "Sessions" ], "description": "Generate PDF of Current Session", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PDFRequest" } } } }, "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PDFResponse" } } } } } } }, "/v1/devtools/inspector.html": { "get": { "operationId": "getDevtoolsUrl", "summary": "Get the URL for the DevTools inspector", "tags": [ "CDP" ], "description": "Get the URL for the DevTools inspector", "parameters": [ { "schema": { "type": "string" }, "in": "query", "name": "pageId", "required": false } ], "responses": { "200": { "description": "Default Response" } } } }, "/v1/sessions/{sessionId}/files": { "post": { "operationId": "upload_file", "summary": "Upload a file", "tags": [ "Files" ], "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.", "requestBody": { "content": { "multipart/form-data": { "schema": { "$ref": "#/components/schemas/FileUploadRequest" } } } }, "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true } ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FileDetails" } } } } } }, "get": { "operationId": "list_files", "summary": "List files", "tags": [ "Files" ], "description": "List all files from the session in descending order.", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true } ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MultipleFiles" } } } } } }, "delete": { "operationId": "delete_all_files", "summary": "Delete all files", "tags": [ "Files" ], "description": "Delete all files from a session", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true } ], "responses": { "204": { "description": "No content" } } } }, "/v1/sessions/{sessionId}/files/{*}": { "get": { "operationId": "download_file", "summary": "Download a file", "tags": [ "Files" ], "description": "Download a file from a session", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true }, { "schema": { "type": "string" }, "in": "path", "name": "*", "required": true } ], "responses": { "200": { "description": "Default Response" } } }, "delete": { "operationId": "delete_file", "summary": "Delete a file", "tags": [ "Files" ], "description": "Delete a file from a session", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true }, { "schema": { "type": "string" }, "in": "path", "name": "*", "required": true } ], "responses": { "204": { "description": "No content" } } } }, "/v1/sessions/{sessionId}/files.zip": { "get": { "operationId": "download_archive", "summary": "Download archive", "tags": [ "Files" ], "description": "Download all files from the session as a zip archive.", "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "sessionId", "required": true } ], "responses": { "200": { "description": "Default Response" } } } }, "/v1/logs/query": { "get": { "tags": [ "Logs" ], "description": "Query browser logs from local storage", "parameters": [ { "schema": { "type": "string", "format": "date-time" }, "in": "query", "name": "startTime", "required": false }, { "schema": { "type": "string", "format": "date-time" }, "in": "query", "name": "endTime", "required": false }, { "schema": { "type": "string" }, "in": "query", "name": "eventTypes", "required": false }, { "schema": { "type": "string" }, "in": "query", "name": "pageId", "required": false }, { "schema": { "type": "string" }, "in": "query", "name": "targetType", "required": false }, { "schema": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100 }, "in": "query", "name": "limit", "required": false }, { "schema": { "type": "integer", "minimum": 0, "default": 0 }, "in": "query", "name": "offset", "required": false } ], "responses": { "200": { "description": "Default Response" } } } }, "/v1/logs/stats": { "get": { "tags": [ "Logs" ], "description": "Get statistics about stored browser logs", "responses": { "200": { "description": "Default Response" } } } }, "/v1/logs/stream": { "get": { "tags": [ "Logs" ], "description": "Stream browser logs in real-time using SSE", "responses": { "200": { "description": "Default Response" } } } }, "/v1/logs/export": { "post": { "tags": [ "Logs" ], "description": "Export browser logs to Parquet format", "parameters": [ { "schema": { "type": "string", "format": "date-time" }, "in": "query", "name": "startTime", "required": false }, { "schema": { "type": "string", "format": "date-time" }, "in": "query", "name": "endTime", "required": false }, { "schema": { "type": "string" }, "in": "query", "name": "eventTypes", "required": false }, { "schema": { "type": "string" }, "in": "query", "name": "pageId", "required": false }, { "schema": { "type": "string" }, "in": "query", "name": "targetType", "required": false }, { "schema": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100 }, "in": "query", "name": "limit", "required": false }, { "schema": { "type": "integer", "minimum": 0, "default": 0 }, "in": "query", "name": "offset", "required": false } ], "responses": { "200": { "description": "Default Response" } } } }, "/v1/logs/": { "delete": { "tags": [ "Logs" ], "description": "Clear all browser logs from storage", "responses": { "200": { "description": "Default Response" } } } } }, "servers": [ { "url": "http://0.0.0.0:3000/", "description": "Local server" }, { "url": "http://0.0.0.0:3000", "description": "Local server from env variables" } ] } ================================================ FILE: api/package.json ================================================ { "name": "@steel-browser/api", "version": "0.5.1", "description": "", "main": "index.js", "type": "module", "private": true, "exports": { "./plugin": { "import": { "types": "./build/steel-browser-plugin.d.ts", "default": "./build/steel-browser-plugin.js" } }, "./cdp-plugin": { "import": { "types": "./build/services/cdp/plugins/core/base-plugin.d.ts", "default": "./build/services/cdp/plugins/core/base-plugin.js" } }, "./logger": { "import": { "types": "./build/plugins/logging/browser-logger.d.ts", "default": "./build/plugins/logging/browser-logger.js" } }, "./storage": { "import": { "types": "./build/services/cdp/instrumentation/storage/log-storage.interface.d.ts", "default": "./build/services/cdp/instrumentation/storage/log-storage.interface.js" } } }, "scripts": { "start": "node ./build/index.js", "build": "tsc && npm run copy:templates && npm run copy:fingerprint", "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 10", "copy:templates": "mkdir -p build/templates && cp -r src/templates/* build/templates/", "copy:fingerprint": "cp src/scripts/fingerprint.js build/scripts/fingerprint.js", "prepare:recorder": "cd extensions/recorder && npm install && npm run build", "dev": "npm run prepare:recorder && tsx watch src/index.ts", "test": "echo \"Error: no test specified\" && exit 1", "pretty": "prettier --write \"src/**/*.ts\"", "generate:openapi": "tsx ./openapi/generate.ts" }, "author": "Nasr Mohamed", "devDependencies": { "@opentelemetry/api": "1.9.0", "@types/archiver": "^6.0.3", "@types/iconv-lite": "^0.0.1", "@types/json-stringify-safe": "^5.0.3", "@types/lodash-es": "^4.17.12", "@types/markdownlint": "^0.13.0", "@types/mime-types": "^2.1.4", "@types/node": "^22.14.1", "@types/turndown": "^5.0.5", "@types/ws": "^8.5.14", "fastify": "^5.2.1", "prettier": "3.0.3", "ts-node": "^10.9.2", "tsx": "^4.19.3", "typescript": "^5.7.3", "vite": "^6.3.5", "vitest": "^3.2.4" }, "dependencies": { "@adobe/css-tools": "^4.4.3", "@fastify/cors": "^10.0.2", "@fastify/multipart": "^9.0.3", "@fastify/reply-from": "^12.0.2", "@fastify/sensible": "^6.0.3", "@fastify/static": "^8.1.1", "@fastify/swagger": "^9.4.2", "@fastify/view": "10.0.2", "@joplin/turndown": "^4.0.80", "@scalar/fastify-api-reference": "^1.25.116", "archiver": "^7.0.1", "axios": "^1.12.0", "cheerio": "^1.1.2", "chokidar": "^4.0.3", "defuddle": "^0.6.4", "dotenv": "^16.4.7", "duckdb-async": "^1.1.3", "ejs": "^3.1.10", "fastify-plugin": "^5.0.1", "file-type": "^20.4.1", "fingerprint-generator": "2.1.72", "fingerprint-injector": "2.1.72", "http-proxy": "^1.18.1", "https-proxy-agent": "^7.0.6", "iconv-lite": "^0.6.3", "jsdom": "^24.1.3", "json-stringify-safe": "^5.0.1", "level": "^9.0.0", "lodash-es": "^4.17.21", "markdownlint": "^0.38.0", "mime-types": "^2.1.35", "pdf2html": "^4.4.0", "pino": "^9.6.0", "pino-pretty": "^13.0.0", "proxy-chain": "^2.5.6", "puppeteer-core": "23.6.0", "socks-proxy-agent": "^8.0.5", "uuid": "^11.0.5", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.1" }, "overrides": { "cross-spawn": "^7.0.6", "tar-fs": ">=3.1.1" }, "peerDependencies": { "@opentelemetry/api": "1.9.0", "fastify": "^5.0.0" }, "peerDependenciesMeta": { "fastify": { "optional": false }, "@opentelemetry/api": { "optional": true } } } ================================================ FILE: api/selenium/driver/LICENSE.chromedriver ================================================ // Copyright 2015 The Chromium Authors // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google LLC nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: api/selenium/driver/THIRD_PARTY_NOTICES.chromedriver ================================================ -------------------- License notice for Google Double Conversion -------------------- Copyright 2006-2011, the V8 project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for QUICHE -------------------- // Copyright 2015 The Chromium Authors. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Abseil -------------------- Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for Implementation of WebDriver BiDi standard -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for mitt -------------------- MIT License Copyright (c) 2021 Jason Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------- License notice for urlpattern-polyfill -------------------- Copyright 2020 Intel Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------- License notice for zod -------------------- MIT License Copyright (c) 2020 Colin McDonnell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -------------------- License notice for WebKit -------------------- (WebKit doesn't distribute an explicit license. This LICENSE is derived from license text in the source.) Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007 Alexander Kellett, Alexey Proskuryakov, Alex Mathews, Allan Sandfeld Jensen, Alp Toker, Anders Carlsson, Andrew Wellington, Antti Koivisto, Apple Inc., Arthur Langereis, Baron Schwartz, Bjoern Graf, Brent Fulgham, Cameron Zwarich, Charles Samuels, Christian Dywan, Collabora Ltd., Cyrus Patel, Daniel Molkentin, Dave Maclachlan, David Smith, Dawit Alemayehu, Dirk Mueller, Dirk Schulze, Don Gibson, Enrico Ros, Eric Seidel, Frederik Holljen, Frerich Raabe, Friedmann Kleint, George Staikos, Google Inc., Graham Dennis, Harri Porten, Henry Mason, Hiroyuki Ikezoe, Holger Hans Peter Freyther, IBM, James G. Speth, Jan Alonzo, Jean-Loup Gailly, John Reis, Jonas Witt, Jon Shier, Jonas Witt, Julien Chaffraix, Justin Haygood, Kevin Ollivier, Kevin Watters, Kimmo Kinnunen, Kouhei Sutou, Krzysztof Kowalczyk, Lars Knoll, Luca Bruno, Maks Orlovich, Malte Starostik, Mark Adler, Martin Jones, Marvin Decker, Matt Lilek, Michael Emmel, Mitz Pettel, mozilla.org, Netscape Communications Corporation, Nicholas Shanks, Nikolas Zimmermann, Nokia, Oliver Hunt, Opened Hand, Paul Johnston, Peter Kelly, Pioneer Research Center USA, Rich Moore, Rob Buis, Robin Dunn, Ronald Tschalär, Samuel Weinig, Simon Hausmann, Staikos Computing Services Inc., Stefan Schimanski, Symantec Corporation, The Dojo Foundation, The Karbon Developers, Thomas Boyer, Tim Copperfield, Tobias Anton, Torben Weis, Trolltech, University of Cambridge, Vaclav Slavik, Waldo Bastian, Xan Lopez, Zack Rusin The terms and conditions vary from file to file, but are one of: Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. *OR* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. GNU LIBRARY GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. GNU LIBRARY GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS -------------------- License notice for BoringSSL -------------------- BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL licensing. Files that are completely new have a Google copyright and an ISC license. This license is reproduced at the bottom of this file. Contributors to BoringSSL are required to follow the CLA rules for Chromium: https://cla.developers.google.com/clas Files in third_party/ have their own licenses, as described therein. The MIT license, for third_party/fiat, which, unlike other third_party directories, is compiled into non-test libraries, is included below. The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the OpenSSL License and the original SSLeay license apply to the toolkit. See below for the actual license texts. Actually both licenses are BSD-style Open Source licenses. In case of any license issues related to OpenSSL please contact openssl-core@openssl.org. The following are Google-internal bug numbers where explicit permission from some authors is recorded for use of their work. (This is purely for our own record keeping.) 27287199 27287880 27287883 263291445 OpenSSL License --------------- /* ==================================================================== * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this * software must display the following acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" * * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * openssl-core@openssl.org. * * 5. Products derived from this software may not be called "OpenSSL" * nor may "OpenSSL" appear in their names without prior written * permission of the OpenSSL Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit (http://www.openssl.org/)" * * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * * This product includes cryptographic software written by Eric Young * (eay@cryptsoft.com). This product includes software written by Tim * Hudson (tjh@cryptsoft.com). * */ Original SSLeay License ----------------------- /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. * * This package is an SSL implementation written * by Eric Young (eay@cryptsoft.com). * The implementation was written so as to conform with Netscapes SSL. * * This library is free for commercial and non-commercial use as long as * the following conditions are aheared to. The following conditions * apply to all code found in this distribution, be it the RC4, RSA, * lhash, DES, etc., code; not just the SSL code. The SSL documentation * included with this distribution is covered by the same copyright terms * except that the holder is Tim Hudson (tjh@cryptsoft.com). * * Copyright remains Eric Young's, and as such any Copyright notices in * the code are not to be removed. * If this package is used in a product, Eric Young should be given attribution * as the author of the parts of the library used. * This can be in the form of a textual message at program startup or * in documentation (online or textual) provided with the package. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * "This product includes cryptographic software written by * Eric Young (eay@cryptsoft.com)" * The word 'cryptographic' can be left out if the rouines from the library * being used are not cryptographic related :-). * 4. If you include any Windows specific code (or a derivative thereof) from * the apps directory (application code) you must include an acknowledgement: * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" * * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The licence and distribution terms for any publically available version or * derivative of this code cannot be changed. i.e. this code cannot simply be * copied and put under another distribution licence * [including the GNU Public Licence.] */ ISC license used for completely new code in BoringSSL: /* Copyright (c) 2015, Google Inc. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ The code in third_party/fiat carries the MIT license: Copyright (c) 2015-2016 the fiat-crypto authors (see https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Licenses for support code ------------------------- Parts of the TLS test suite are under the Go license. This code is not included in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so distributing code linked against BoringSSL does not trigger this license: Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. BoringSSL uses the Chromium test infrastructure to run a continuous build, trybots etc. The scripts which manage this, and the script for generating build metadata, are under the Chromium license. Distributing code linked against BoringSSL does not trigger this license. Copyright 2015 The Chromium Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Fiat-Crypto: Synthesizing Correct-by-Construction Code for Cryptographic Primitives -------------------- The MIT License (MIT) Copyright (c) 2015-2020 the fiat-crypto authors (see https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------- License notice for Brotli -------------------- Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------- License notice for Compact Encoding Detection -------------------- // Copyright 2010 The Chromium Authors // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google LLC nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Crashpad -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for CRC32C -------------------- Copyright 2017, The CRC32C Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for d3 -------------------- Copyright 2010-2023 Mike Bostock Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------- License notice for dav1d is an AV1 decoder :) -------------------- Copyright © 2018, VideoLAN and dav1d authors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Dawn -------------------- // Copyright 2017-2023 The Dawn & Tint Authors // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this // list of conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Expat XML Parser -------------------- Copyright (c) 1998-2000 Thai Open Source Software Center Ltd and Clark Cooper Copyright (c) 2001-2022 Expat maintainers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------- License notice for ffmpeg -------------------- # License Most files in FFmpeg are under the GNU Lesser General Public License version 2.1 or later (LGPL v2.1+). Read the file `COPYING.LGPLv2.1` for details. Some other files have MIT/X11/BSD-style licenses. In combination the LGPL v2.1+ applies to FFmpeg. Some optional parts of FFmpeg are licensed under the GNU General Public License version 2 or later (GPL v2+). See the file `COPYING.GPLv2` for details. None of these parts are used by default, you have to explicitly pass `--enable-gpl` to configure to activate them. In this case, FFmpeg's license changes to GPL v2+. Specifically, the GPL parts of FFmpeg are: - libpostproc - optional x86 optimization in the files - `libavcodec/x86/flac_dsp_gpl.asm` - `libavcodec/x86/idct_mmx.c` - `libavfilter/x86/vf_removegrain.asm` - the following building and testing tools - `compat/solaris/make_sunver.pl` - `doc/t2h.pm` - `doc/texi2pod.pl` - `libswresample/tests/swresample.c` - `tests/checkasm/*` - `tests/tiny_ssim.c` - the following filters in libavfilter: - `signature_lookup.c` - `vf_blackframe.c` - `vf_boxblur.c` - `vf_colormatrix.c` - `vf_cover_rect.c` - `vf_cropdetect.c` - `vf_delogo.c` - `vf_eq.c` - `vf_find_rect.c` - `vf_fspp.c` - `vf_histeq.c` - `vf_hqdn3d.c` - `vf_kerndeint.c` - `vf_lensfun.c` (GPL version 3 or later) - `vf_mcdeint.c` - `vf_mpdecimate.c` - `vf_nnedi.c` - `vf_owdenoise.c` - `vf_perspective.c` - `vf_phase.c` - `vf_pp.c` - `vf_pp7.c` - `vf_pullup.c` - `vf_repeatfields.c` - `vf_sab.c` - `vf_signature.c` - `vf_smartblur.c` - `vf_spp.c` - `vf_stereo3d.c` - `vf_super2xsai.c` - `vf_tinterlace.c` - `vf_uspp.c` - `vf_vaguedenoiser.c` - `vsrc_mptestsrc.c` Should you, for whatever reason, prefer to use version 3 of the (L)GPL, then the configure parameter `--enable-version3` will activate this licensing option for you. Read the file `COPYING.LGPLv3` or, if you have enabled GPL parts, `COPYING.GPLv3` to learn the exact legal terms that apply in this case. There are a handful of files under other licensing terms, namely: * The files `libavcodec/jfdctfst.c`, `libavcodec/jfdctint_template.c` and `libavcodec/jrevdct.c` are taken from libjpeg, see the top of the files for licensing details. Specifically note that you must credit the IJG in the documentation accompanying your program if you only distribute executables. You must also indicate any changes including additions and deletions to those three files in the documentation. * `tests/reference.pnm` is under the expat license. ## External libraries FFmpeg can be combined with a number of external libraries, which sometimes affect the licensing of binaries resulting from the combination. ### Compatible libraries The following libraries are under GPL version 2: - avisynth - frei0r - libcdio - libdavs2 - librubberband - libvidstab - libx264 - libx265 - libxavs - libxavs2 - libxvid When combining them with FFmpeg, FFmpeg needs to be licensed as GPL as well by passing `--enable-gpl` to configure. The following libraries are under LGPL version 3: - gmp - libaribb24 - liblensfun When combining them with FFmpeg, use the configure option `--enable-version3` to upgrade FFmpeg to the LGPL v3. The VMAF, mbedTLS, RK MPI, OpenCORE and VisualOn libraries are under the Apache License 2.0. That license is incompatible with the LGPL v2.1 and the GPL v2, but not with version 3 of those licenses. So to combine these libraries with FFmpeg, the license version needs to be upgraded by passing `--enable-version3` to configure. The smbclient library is under the GPL v3, to combine it with FFmpeg, the options `--enable-gpl` and `--enable-version3` have to be passed to configure to upgrade FFmpeg to the GPL v3. ### Incompatible libraries There are certain libraries you can combine with FFmpeg whose licenses are not compatible with the GPL and/or the LGPL. If you wish to enable these libraries, even in circumstances that their license may be incompatible, pass `--enable-nonfree` to configure. This will cause the resulting binary to be unredistributable. The Fraunhofer FDK AAC and OpenSSL libraries are under licenses which are incompatible with the GPLv2 and v3. To the best of our knowledge, they are compatible with the LGPL. ******************************************************************************** libavformat/oggparsetheora.c Copyright (C) 2005 Matthieu CASTET, Alex Beregszaszi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ******************************************************************************** libavutil/x86/x86inc.asm x86inc.asm: x86 abstraction layer Copyright (C) 2005-2024 x264 project Authors: Loren Merritt Henrik Gramner Anton Mitrofanov Fiona Glaser Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ******************************************************************************** libavcodec/mips/compute_antialias_fixed.h libavcodec/mips/compute_antialias_float.h libavutil/fixed_dsp.c libavutil/fixed_dsp.h libavutil/mips/libm_mips.h libavutil/softfloat_tables.h Copyright (c) 2012 MIPS Technologies, Inc., California. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the MIPS Technologies, Inc., nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE MIPS TECHNOLOGIES, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE MIPS TECHNOLOGIES, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Authors: Branimir Vasic (bvasic@mips.com) Darko Laus (darko@mips.com) Djordje Pesut (djordje@mips.com) Goran Cordasic (goran@mips.com) Nedeljko Babic (nedeljko.babic imgtec com) Mirjana Vulin (mvulin@mips.com) Stanislav Ocovaj (socovaj@mips.com) Zoran Lukic (zoranl@mips.com) ******************************************************************************** libavformat/oggdec.c libavformat/oggdec.h libavformat/oggparseogm.c libavformat/oggparsevorbis.c Copyright (C) 2005 Michael Ahlberg, MÃ¥ns RullgÃ¥rd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ******************************************************************************** GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! -------------------- License notice for FreeType -------------------- The FreeType Project LICENSE ---------------------------- 2006-Jan-27 Copyright 1996-2002, 2006 by David Turner, Robert Wilhelm, and Werner Lemberg Introduction ============ The FreeType Project is distributed in several archive packages; some of them may contain, in addition to the FreeType font engine, various tools and contributions which rely on, or relate to, the FreeType Project. This license applies to all files found in such packages, and which do not fall under their own explicit license. The license affects thus the FreeType font engine, the test programs, documentation and makefiles, at the very least. This license was inspired by the BSD, Artistic, and IJG (Independent JPEG Group) licenses, which all encourage inclusion and use of free software in commercial and freeware products alike. As a consequence, its main points are that: o We don't promise that this software works. However, we will be interested in any kind of bug reports. (`as is' distribution) o You can use this software for whatever you want, in parts or full form, without having to pay us. (`royalty-free' usage) o You may not pretend that you wrote this software. If you use it, or only parts of it, in a program, you must acknowledge somewhere in your documentation that you have used the FreeType code. (`credits') We specifically permit and encourage the inclusion of this software, with or without modifications, in commercial products. We disclaim all warranties covering The FreeType Project and assume no liability related to The FreeType Project. Finally, many people asked us for a preferred form for a credit/disclaimer to use in compliance with this license. We thus encourage you to use the following text: """ Portions of this software are copyright © The FreeType Project (www.freetype.org). All rights reserved. """ Please replace with the value from the FreeType version you actually use. Legal Terms =========== 0. Definitions -------------- Throughout this license, the terms `package', `FreeType Project', and `FreeType archive' refer to the set of files originally distributed by the authors (David Turner, Robert Wilhelm, and Werner Lemberg) as the `FreeType Project', be they named as alpha, beta or final release. `You' refers to the licensee, or person using the project, where `using' is a generic term including compiling the project's source code as well as linking it to form a `program' or `executable'. This program is referred to as `a program using the FreeType engine'. This license applies to all files distributed in the original FreeType Project, including all source code, binaries and documentation, unless otherwise stated in the file in its original, unmodified form as distributed in the original archive. If you are unsure whether or not a particular file is covered by this license, you must contact us to verify this. The FreeType Project is copyright (C) 1996-2000 by David Turner, Robert Wilhelm, and Werner Lemberg. All rights reserved except as specified below. 1. No Warranty -------------- THE FREETYPE PROJECT IS PROVIDED `AS IS' WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT WILL ANY OF THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DAMAGES CAUSED BY THE USE OR THE INABILITY TO USE, OF THE FREETYPE PROJECT. 2. Redistribution ----------------- This license grants a worldwide, royalty-free, perpetual and irrevocable right and license to use, execute, perform, compile, display, copy, create derivative works of, distribute and sublicense the FreeType Project (in both source and object code forms) and derivative works thereof for any purpose; and to authorize others to exercise some or all of the rights granted herein, subject to the following conditions: o Redistribution of source code must retain this license file (`FTL.TXT') unaltered; any additions, deletions or changes to the original files must be clearly indicated in accompanying documentation. The copyright notices of the unaltered, original files must be preserved in all copies of source files. o Redistribution in binary form must provide a disclaimer that states that the software is based in part of the work of the FreeType Team, in the distribution documentation. We also encourage you to put an URL to the FreeType web page in your documentation, though this isn't mandatory. These conditions apply to any software derived from or based on the FreeType Project, not just the unmodified files. If you use our work, you must acknowledge us. However, no fee need be paid to us. 3. Advertising -------------- Neither the FreeType authors and contributors nor you shall use the name of the other for commercial, advertising, or promotional purposes without specific prior written permission. We suggest, but do not require, that you use one or more of the following phrases to refer to this software in your documentation or advertising materials: `FreeType Project', `FreeType Engine', `FreeType library', or `FreeType Distribution'. As you have not signed this license, you are not required to accept it. However, as the FreeType Project is copyrighted material, only this license, or another one contracted with the authors, grants you the right to use, distribute, and modify it. Therefore, by using, distributing, or modifying the FreeType Project, you indicate that you understand and accept all the terms of this license. 4. Contacts ----------- There are two mailing lists related to FreeType: o freetype@nongnu.org Discusses general use and applications of FreeType, as well as future and wanted additions to the library and distribution. If you are looking for support, start in this list if you haven't found anything to help you in the documentation. o freetype-devel@nongnu.org Discusses bugs, as well as engine internals, design issues, specific licenses, porting, etc. Our home page can be found at https://www.freetype.org --- end of FTL.TXT --- -------------------- License notice for harfbuzz-ng -------------------- HarfBuzz is licensed under the so-called "Old MIT" license. Details follow. For parts of HarfBuzz that are licensed under different licenses see individual files names COPYING in subdirectories where applicable. Copyright © 2010-2022 Google, Inc. Copyright © 2015-2020 Ebrahim Byagowi Copyright © 2019,2020 Facebook, Inc. Copyright © 2012,2015 Mozilla Foundation Copyright © 2011 Codethink Limited Copyright © 2008,2010 Nokia Corporation and/or its subsidiary(-ies) Copyright © 2009 Keith Stribley Copyright © 2011 Martin Hosken and SIL International Copyright © 2007 Chris Wilson Copyright © 2005,2006,2020,2021,2022,2023 Behdad Esfahbod Copyright © 2004,2007,2008,2009,2010,2013,2021,2022,2023 Red Hat, Inc. Copyright © 1998-2005 David Turner and Werner Lemberg Copyright © 2016 Igalia S.L. Copyright © 2022 Matthias Clasen Copyright © 2018,2021 Khaled Hosny Copyright © 2018,2019,2020 Adobe, Inc Copyright © 2013-2015 Alexei Podtelezhnikov For full copyright notices consult the individual files in the package. Permission is hereby granted, without written agreement and without license or royalty fees, to use, copy, modify, and distribute this software and its documentation for any purpose, provided that the above copyright notice and the following two paragraphs appear in all copies of this software. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. -------------------- License notice for icu -------------------- UNICODE LICENSE V3 COPYRIGHT AND PERMISSION NOTICE Copyright © 2016-2023 Unicode, Inc. NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. Permission is hereby granted, free of charge, to any person obtaining a copy of data files and any associated documentation (the "Data Files") or software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Data Files or Software, and to permit persons to whom the Data Files or Software are furnished to do so, provided that either (a) this copyright and permission notice appear with all copies of the Data Files or Software, or (b) this copyright and permission notice appear in associated Documentation. THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE. Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. ---------------------------------------------------------------------- Third-Party Software Licenses This section contains third-party software notices and/or additional terms for licensed third-party software components included within ICU libraries. ---------------------------------------------------------------------- ICU License - ICU 1.8.1 to ICU 57.1 COPYRIGHT AND PERMISSION NOTICE Copyright (c) 1995-2016 International Business Machines Corporation and others All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, provided that the above copyright notice(s) and this permission notice appear in all copies of the Software and that both the above copyright notice(s) and this permission notice appear in supporting documentation. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization of the copyright holder. All trademarks and registered trademarks mentioned herein are the property of their respective owners. ---------------------------------------------------------------------- Chinese/Japanese Word Break Dictionary Data (cjdict.txt) # The Google Chrome software developed by Google is licensed under # the BSD license. Other software included in this distribution is # provided under other licenses, as set forth below. # # The BSD License # http://opensource.org/licenses/bsd-license.php # Copyright (C) 2006-2008, Google Inc. # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided with # the distribution. # Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # The word list in cjdict.txt are generated by combining three word lists # listed below with further processing for compound word breaking. The # frequency is generated with an iterative training against Google web # corpora. # # * Libtabe (Chinese) # - https://sourceforge.net/project/?group_id=1519 # - Its license terms and conditions are shown below. # # * IPADIC (Japanese) # - http://chasen.aist-nara.ac.jp/chasen/distribution.html # - Its license terms and conditions are shown below. # # ---------COPYING.libtabe ---- BEGIN-------------------- # # /* # * Copyright (c) 1999 TaBE Project. # * Copyright (c) 1999 Pai-Hsiang Hsiao. # * All rights reserved. # * # * Redistribution and use in source and binary forms, with or without # * modification, are permitted provided that the following conditions # * are met: # * # * . Redistributions of source code must retain the above copyright # * notice, this list of conditions and the following disclaimer. # * . Redistributions in binary form must reproduce the above copyright # * notice, this list of conditions and the following disclaimer in # * the documentation and/or other materials provided with the # * distribution. # * . Neither the name of the TaBE Project nor the names of its # * contributors may be used to endorse or promote products derived # * from this software without specific prior written permission. # * # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # * OF THE POSSIBILITY OF SUCH DAMAGE. # */ # # /* # * Copyright (c) 1999 Computer Systems and Communication Lab, # * Institute of Information Science, Academia # * Sinica. All rights reserved. # * # * Redistribution and use in source and binary forms, with or without # * modification, are permitted provided that the following conditions # * are met: # * # * . Redistributions of source code must retain the above copyright # * notice, this list of conditions and the following disclaimer. # * . Redistributions in binary form must reproduce the above copyright # * notice, this list of conditions and the following disclaimer in # * the documentation and/or other materials provided with the # * distribution. # * . Neither the name of the Computer Systems and Communication Lab # * nor the names of its contributors may be used to endorse or # * promote products derived from this software without specific # * prior written permission. # * # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # * OF THE POSSIBILITY OF SUCH DAMAGE. # */ # # Copyright 1996 Chih-Hao Tsai @ Beckman Institute, # University of Illinois # c-tsai4@uiuc.edu http://casper.beckman.uiuc.edu/~c-tsai4 # # ---------------COPYING.libtabe-----END-------------------------------- # # # ---------------COPYING.ipadic-----BEGIN------------------------------- # # Copyright 2000, 2001, 2002, 2003 Nara Institute of Science # and Technology. All Rights Reserved. # # Use, reproduction, and distribution of this software is permitted. # Any copy of this software, whether in its original form or modified, # must include both the above copyright notice and the following # paragraphs. # # Nara Institute of Science and Technology (NAIST), # the copyright holders, disclaims all warranties with regard to this # software, including all implied warranties of merchantability and # fitness, in no event shall NAIST be liable for # any special, indirect or consequential damages or any damages # whatsoever resulting from loss of use, data or profits, whether in an # action of contract, negligence or other tortuous action, arising out # of or in connection with the use or performance of this software. # # A large portion of the dictionary entries # originate from ICOT Free Software. The following conditions for ICOT # Free Software applies to the current dictionary as well. # # Each User may also freely distribute the Program, whether in its # original form or modified, to any third party or parties, PROVIDED # that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear # on, or be attached to, the Program, which is distributed substantially # in the same form as set out herein and that such intended # distribution, if actually made, will neither violate or otherwise # contravene any of the laws and regulations of the countries having # jurisdiction over the User or the intended distribution itself. # # NO WARRANTY # # The program was produced on an experimental basis in the course of the # research and development conducted during the project and is provided # to users as so produced on an experimental basis. Accordingly, the # program is provided without any warranty whatsoever, whether express, # implied, statutory or otherwise. The term "warranty" used herein # includes, but is not limited to, any warranty of the quality, # performance, merchantability and fitness for a particular purpose of # the program and the nonexistence of any infringement or violation of # any right of any third party. # # Each user of the program will agree and understand, and be deemed to # have agreed and understood, that there is no warranty whatsoever for # the program and, accordingly, the entire risk arising from or # otherwise connected with the program is assumed by the user. # # Therefore, neither ICOT, the copyright holder, or any other # organization that participated in or was otherwise related to the # development of the program and their respective officials, directors, # officers and other employees shall be held liable for any and all # damages, including, without limitation, general, special, incidental # and consequential damages, arising out of or otherwise in connection # with the use or inability to use the program or any product, material # or result produced or otherwise obtained by using the program, # regardless of whether they have been advised of, or otherwise had # knowledge of, the possibility of such damages at any time during the # project or thereafter. Each user will be deemed to have agreed to the # foregoing by his or her commencement of use of the program. The term # "use" as used herein includes, but is not limited to, the use, # modification, copying and distribution of the program and the # production of secondary products from the program. # # In the case where the program, whether in its original form or # modified, was distributed or delivered to or received by a user from # any person, organization or entity other than ICOT, unless it makes or # grants independently of ICOT any specific warranty to the user in # writing, such person, organization or entity, will also be exempted # from and not be held liable to the user for any such damages as noted # above as far as the program is concerned. # # ---------------COPYING.ipadic-----END---------------------------------- ---------------------------------------------------------------------- Lao Word Break Dictionary Data (laodict.txt) # Copyright (C) 2016 and later: Unicode, Inc. and others. # License & terms of use: http://www.unicode.org/copyright.html # Copyright (c) 2015 International Business Machines Corporation # and others. All Rights Reserved. # # Project: https://github.com/rober42539/lao-dictionary # Dictionary: https://github.com/rober42539/lao-dictionary/laodict.txt # License: https://github.com/rober42539/lao-dictionary/LICENSE.txt # (copied below) # # This file is derived from the above dictionary version of Nov 22, 2020 # ---------------------------------------------------------------------- # Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. Redistributions in binary # form must reproduce the above copyright notice, this list of conditions and # the following disclaimer in the documentation and/or other materials # provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED # OF THE POSSIBILITY OF SUCH DAMAGE. # -------------------------------------------------------------------------- ---------------------------------------------------------------------- Burmese Word Break Dictionary Data (burmesedict.txt) # Copyright (c) 2014 International Business Machines Corporation # and others. All Rights Reserved. # # This list is part of a project hosted at: # github.com/kanyawtech/myanmar-karen-word-lists # # -------------------------------------------------------------------------- # Copyright (c) 2013, LeRoy Benjamin Sharon # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: Redistributions of source code must retain the above # copyright notice, this list of conditions and the following # disclaimer. Redistributions in binary form must reproduce the # above copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # Neither the name Myanmar Karen Word Lists, nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # -------------------------------------------------------------------------- ---------------------------------------------------------------------- Time Zone Database ICU uses the public domain data and code derived from Time Zone Database for its time zone support. The ownership of the TZ database is explained in BCP 175: Procedure for Maintaining the Time Zone Database section 7. # 7. Database Ownership # # The TZ database itself is not an IETF Contribution or an IETF # document. Rather it is a pre-existing and regularly updated work # that is in the public domain, and is intended to remain in the # public domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do # not apply to the TZ Database or contributions that individuals make # to it. Should any claims be made and substantiated against the TZ # Database, the organization that is providing the IANA # Considerations defined in this RFC, under the memorandum of # understanding with the IETF, currently ICANN, may act in accordance # with all competent court orders. No ownership claims will be made # by ICANN or the IETF Trust on the database or the code. Any person # making a contribution to the database or code waives all rights to # future claims in that contribution or in the TZ Database. ---------------------------------------------------------------------- Google double-conversion Copyright 2006-2011, the V8 project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------- File: aclocal.m4 (only for ICU4C) Section: pkg.m4 - Macros to locate and utilise pkg-config. Copyright © 2004 Scott James Remnant . Copyright © 2012-2015 Dan Nicholson This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. As a special exception to the GNU General Public License, if you distribute this file as part of a program that contains a configuration script generated by Autoconf, you may include it under the same distribution terms that you use for the rest of that program. (The condition for the exception is fulfilled because ICU4C includes a configuration script generated by Autoconf, namely the `configure` script.) ---------------------------------------------------------------------- File: config.guess (only for ICU4C) This file is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . As a special exception to the GNU General Public License, if you distribute this file as part of a program that contains a configuration script generated by Autoconf, you may include it under the same distribution terms that you use for the rest of that program. This Exception is an additional permission under section 7 of the GNU General Public License, version 3 ("GPLv3"). (The condition for the exception is fulfilled because ICU4C includes a configuration script generated by Autoconf, namely the `configure` script.) ---------------------------------------------------------------------- File: install-sh (only for ICU4C) Copyright 1991 by the Massachusetts Institute of Technology Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of M.I.T. not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. M.I.T. makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. -------------------- License notice for ipcz -------------------- // Copyright 2022 The Chromium Authors // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google LLC nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for jsoncpp -------------------- The JsonCpp library's source code, including accompanying documentation, tests and demonstration applications, are licensed under the following conditions... The author (Baptiste Lepilleur) explicitly disclaims copyright in all jurisdictions which recognize such a disclaimer. In such jurisdictions, this software is released into the Public Domain. In jurisdictions which do not recognize Public Domain property (e.g. Germany as of 2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is released under the terms of the MIT License (see below). In jurisdictions which recognize Public Domain property, the user of this software may choose to accept it either as 1) Public Domain, 2) under the conditions of the MIT License (see below), or 3) under the terms of dual Public Domain/MIT License conditions described here, as they choose. The MIT License is about as close to Public Domain as a license can get, and is described in clear, concise terms at: http://en.wikipedia.org/wiki/MIT_License The full text of the MIT License follows: ======================================================================== Copyright (c) 2007-2010 Baptiste Lepilleur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== (END LICENSE TEXT) The MIT license is compatible with both the GPL and commercial software, affording one all of the rights of Public Domain with the minor nuisance of being required to keep the above copyright notice and license text in the source code. Note also that by accepting the Public Domain "license" you can re-license your copy using whatever license you like. -------------------- License notice for google-jstemplate -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for Alliance for Open Media Video Codec -------------------- Copyright (c) 2016, Alliance for Open Media. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for libjpeg-turbo -------------------- libjpeg-turbo Licenses ====================== libjpeg-turbo is covered by three compatible BSD-style open source licenses: - The IJG (Independent JPEG Group) License, which is listed in [README.ijg](README.ijg) This license applies to the libjpeg API library and associated programs (any code inherited from libjpeg, and any modifications to that code.) - The Modified (3-clause) BSD License, which is listed below This license covers the TurboJPEG API library and associated programs, as well as the build system. - The [zlib License](https://opensource.org/licenses/Zlib) This license is a subset of the other two, and it covers the libjpeg-turbo SIMD extensions. Complying with the libjpeg-turbo Licenses ========================================= This section provides a roll-up of the libjpeg-turbo licensing terms, to the best of our understanding. 1. If you are distributing a modified version of the libjpeg-turbo source, then: 1. You cannot alter or remove any existing copyright or license notices from the source. **Origin** - Clause 1 of the IJG License - Clause 1 of the Modified BSD License - Clauses 1 and 3 of the zlib License 2. You must add your own copyright notice to the header of each source file you modified, so others can tell that you modified that file (if there is not an existing copyright header in that file, then you can simply add a notice stating that you modified the file.) **Origin** - Clause 1 of the IJG License - Clause 2 of the zlib License 3. You must include the IJG README file, and you must not alter any of the copyright or license text in that file. **Origin** - Clause 1 of the IJG License 2. If you are distributing only libjpeg-turbo binaries without the source, or if you are distributing an application that statically links with libjpeg-turbo, then: 1. Your product documentation must include a message stating: This software is based in part on the work of the Independent JPEG Group. **Origin** - Clause 2 of the IJG license 2. If your binary distribution includes or uses the TurboJPEG API, then your product documentation must include the text of the Modified BSD License (see below.) **Origin** - Clause 2 of the Modified BSD License 3. You cannot use the name of the IJG or The libjpeg-turbo Project or the contributors thereof in advertising, publicity, etc. **Origin** - IJG License - Clause 3 of the Modified BSD License 4. The IJG and The libjpeg-turbo Project do not warrant libjpeg-turbo to be free of defects, nor do we accept any liability for undesirable consequences resulting from your use of the software. **Origin** - IJG License - Modified BSD License - zlib License The Modified (3-clause) BSD License =================================== Copyright (C)2009-2023 D. R. Commander. All Rights Reserved.
Copyright (C)2015 Viktor Szathmáry. All Rights Reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the libjpeg-turbo Project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Why Three Licenses? =================== The zlib License could have been used instead of the Modified (3-clause) BSD License, and since the IJG License effectively subsumes the distribution conditions of the zlib License, this would have effectively placed libjpeg-turbo binary distributions under the IJG License. However, the IJG License specifically refers to the Independent JPEG Group and does not extend attribution and endorsement protections to other entities. Thus, it was desirable to choose a license that granted us the same protections for new code that were granted to the IJG for code derived from their software. -------------------- License notice for libpng -------------------- COPYRIGHT NOTICE, DISCLAIMER, and LICENSE ========================================= PNG Reference Library License version 2 --------------------------------------- * Copyright (c) 1995-2019 The PNG Reference Library Authors. * Copyright (c) 2018-2019 Cosmin Truta. * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. * Copyright (c) 1996-1997 Andreas Dilger. * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. The software is supplied "as is", without warranty of any kind, express or implied, including, without limitation, the warranties of merchantability, fitness for a particular purpose, title, and non-infringement. In no event shall the Copyright owners, or anyone distributing the software, be liable for any damages or other liability, whether in contract, tort or otherwise, arising from, out of, or in connection with the software, or the use or other dealings in the software, even if advised of the possibility of such damage. Permission is hereby granted to use, copy, modify, and distribute this software, or portions hereof, for any purpose, without fee, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated, but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This Copyright notice may not be removed or altered from any source or altered source distribution. PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) ----------------------------------------------------------------------- libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are derived from libpng-1.0.6, and are distributed according to the same disclaimer and license as libpng-1.0.6 with the following individuals added to the list of Contributing Authors: Simon-Pierre Cadieux Eric S. Raymond Mans Rullgard Cosmin Truta Gilles Vollant James Yu Mandar Sahastrabuddhe Google Inc. Vadim Barkov and with the following additions to the disclaimer: There is no warranty against interference with your enjoyment of the library or against infringement. There is no warranty that our efforts or the library will fulfill any of your particular purposes or needs. This library is provided with all faults, and the entire risk of satisfactory quality, performance, accuracy, and effort is with the user. Some files in the "contrib" directory and some configure-generated files that are distributed with libpng have other copyright owners, and are released under other open source licenses. libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from libpng-0.96, and are distributed according to the same disclaimer and license as libpng-0.96, with the following individuals added to the list of Contributing Authors: Tom Lane Glenn Randers-Pehrson Willem van Schaik libpng versions 0.89, June 1996, through 0.96, May 1997, are Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, and are distributed according to the same disclaimer and license as libpng-0.88, with the following individuals added to the list of Contributing Authors: John Bowler Kevin Bracey Sam Bushell Magnus Holmgren Greg Roelofs Tom Tanner Some files in the "scripts" directory have other copyright owners, but are released under this license. libpng versions 0.5, May 1995, through 0.88, January 1996, are Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. For the purposes of this copyright and license, "Contributing Authors" is defined as the following set of individuals: Andreas Dilger Dave Martindale Guy Eric Schalnat Paul Schmidt Tim Wegner The PNG Reference Library is supplied "AS IS". The Contributing Authors and Group 42, Inc. disclaim all warranties, expressed or implied, including, without limitation, the warranties of merchantability and of fitness for any purpose. The Contributing Authors and Group 42, Inc. assume no liability for direct, indirect, incidental, special, exemplary, or consequential damages, which may result from the use of the PNG Reference Library, even if advised of the possibility of such damage. Permission is hereby granted to use, copy, modify, and distribute this source code, or portions hereof, for any purpose, without fee, subject to the following restrictions: 1. The origin of this source code must not be misrepresented. 2. Altered versions must be plainly marked as such and must not be misrepresented as being the original source. 3. This Copyright notice may not be removed or altered from any source or altered source distribution. The Contributing Authors and Group 42, Inc. specifically permit, without fee, and encourage the use of this source code as a component to supporting the PNG file format in commercial products. If you use this source code in a product, acknowledgment is not required but would be appreciated. -------------------- License notice for libsrtp -------------------- /* * * Copyright (c) 2001-2017 Cisco Systems, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the Cisco Systems, Inc. nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * */ -------------------- License notice for URL Pattern Library -------------------- The MIT License (MIT) Copyright 2020 The Chromium Authors Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------- License notice for libvpx -------------------- Copyright (c) 2010, The WebM Project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google, nor the WebM Project, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for WebP image encoder/decoder -------------------- Copyright (c) 2010, Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Additional IP Rights Grant (Patents) ------------------------------------ "These implementations" means the copyrightable works that implement the WebM codecs distributed by Google as part of the WebM Project. Google 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, transfer, and otherwise run, modify and propagate the contents of these implementations of WebM, where such license applies only to those patent claims, both currently owned by Google and acquired in the future, licensable by Google that are necessarily infringed by these implementations of WebM. This grant does not include claims that would be infringed only as a consequence of further modification of these implementations. If you or your agent or exclusive licensee institute or order or agree to the institution of patent litigation or any other patent enforcement activity against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that any of these implementations of WebM or any code incorporated within any of these implementations of WebM constitute direct or contributory patent infringement, or inducement of patent infringement, then any patent rights granted to you under this License for these implementations of WebM shall terminate as of the date such litigation is filed. -------------------- License notice for libyuv -------------------- Copyright 2011 The LibYuv Project Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Lit -------------------- BSD 3-Clause License Copyright (c) 2017 Google LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Lottie Web -------------------- The MIT License (MIT) Copyright (c) 2015 Bodymovin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ################################################################################ # License headers for subpackages ################################################################################ Transformation Matrix v2.0 (c) Epistemex 2014-2015 www.epistemex.com By Ken Fyrstenberg Contributions by leeoniya. License: MIT, header required. ################################################################################ Copyright 2014 David Bau. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ################################################################################ BezierEasing - use bezier curve for transition easing function by Gaëtan Renaudeau 2014 - 2015 – MIT License Credits: is based on Firefox's nsSMILKeySpline.cpp Usage: var spline = BezierEasing([ 0.25, 0.1, 0.25, 1.0 ]) spline.get(x) => returns the easing value | x must be in [0, 1] range -------------------- License notice for Metrics Protos -------------------- // Copyright 2015 The Chromium Authors // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for modp base64 decoder -------------------- * MODP_B64 - High performance base64 encoder/decoder * Version 1.3 -- 17-Mar-2006 * http://modp.com/release/base64 * * Copyright (c) 2005, 2006 Nick Galbreath -- nickg [at] modp [dot] com * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the modp.com nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for OpenH264 -------------------- Copyright (c) 2013, Cisco Systems All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for opus -------------------- Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of Internet Society, IETF or IETF Trust, nor the names of specific contributors, may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Opus is subject to the royalty-free patent licenses which are specified at: Xiph.Org Foundation: https://datatracker.ietf.org/ipr/1524/ Microsoft Corporation: https://datatracker.ietf.org/ipr/1914/ Broadcom Corporation: https://datatracker.ietf.org/ipr/1526/ -------------------- License notice for Perfetto -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS Copyright (c) 2017, The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Copyright 2015 The Chromium Authors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for PFFFT: a pretty fast FFT. -------------------- Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) Based on original fortran 77 code from FFTPACKv4 from NETLIB, authored by Dr Paul Swarztrauber of NCAR, in 1985. As confirmed by the NCAR fftpack software curators, the following FFTPACKv5 license applies to FFTPACKv4 sources. My changes are released under the same terms. FFTPACK license: http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html Copyright (c) 2004 the University Corporation for Atmospheric Research ("UCAR"). All rights reserved. Developed by NCAR's Computational and Information Systems Laboratory, UCAR, www.cisl.ucar.edu. Redistribution and use of the Software in source and binary forms, with or without modification, is permitted provided that the following conditions are met: - Neither the names of NCAR's Computational and Information Systems Laboratory, the University Corporation for Atmospheric Research, nor the names of its sponsors or contributors may be used to endorse or promote products derived from this Software without specific prior written permission. - Redistributions of source code must retain the above copyright notices, this list of conditions, and the disclaimer below. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the disclaimer below in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. -------------------- License notice for Polymer -------------------- // Copyright (c) 2012 The Polymer Authors. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Protocol Buffers -------------------- Copyright 2008 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Code generated by the Protocol Buffer compiler is owned by the owner of the input file used when generating it. This code is not standalone and requires a support library to be linked with it. This support library is itself covered by the above license. -------------------- License notice for Protocol Buffers (javascript) -------------------- BSD 3-Clause License Copyright (c) 2022, Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for re2 - an efficient, principled regular expression library -------------------- // Copyright (c) 2009 The RE2 Authors. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for Recurrent neural network for audio noise reduction -------------------- Copyright (c) 2017, Mozilla Copyright (c) 2007-2017, Jean-Marc Valin Copyright (c) 2005-2017, Xiph.Org Foundation Copyright (c) 2003-2004, Mark Borgerding Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Xiph.Org Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for bytemuck -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for bytemuck_derive -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for cxx -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for cxxbridge-macro -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for font-types -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and 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 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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS Copyright 2019 Colin Rothfels Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for itoa -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for proc-macro2 -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for quote -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for read-fonts -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and 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 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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS Copyright 2019 Colin Rothfels Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for ryu -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for serde -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for serde_derive -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for serde_json_lenient -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for skrifa -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and 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 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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS Copyright 2019 Colin Rothfels Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for syn -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for unicode-ident -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS -------------------- License notice for unicode-ident -------------------- UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE See Terms of Use for definitions of Unicode Inc.’s Data Files and Software. NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. COPYRIGHT AND PERMISSION NOTICE Copyright © 1991-2022 Unicode, Inc. All rights reserved. Distributed under the Terms of Use in https://www.unicode.org/copyright.html. Permission is hereby granted, free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Data Files or Software, and to permit persons to whom the Data Files or Software are furnished to do so, provided that either (a) this copyright and permission notice appear with all copies of the Data Files or Software, or (b) this copyright and permission notice appear in associated Documentation. THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE. Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. -------------------- License notice for Selenium Atoms -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright 2022 Software Freedom Conservancy (SFC) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for Sizzle -------------------- MIT License ---- Copyright (c) 2009 John Resig Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------- License notice for Wicked Good XPath -------------------- The MIT License Copyright (c) 2007 Cybozu Labs, Inc. Copyright (c) 2012 Google Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------- License notice for sqlite -------------------- The author disclaims copyright to this source code. In place of a legal notice, here is a blessing: May you do good and not evil. May you find forgiveness for yourself and forgive others. May you share freely, never taking more than you give. -------------------- License notice for Vulkan API headers -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for WebRTC -------------------- Copyright (c) 2011, The WebRTC project authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------- License notice for General Purpose FFT (Fast Fourier/Cosine/Sine Transform) Package -------------------- /* * http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html * Copyright Takuya OOURA, 1996-2001 * * You may use, copy, modify and distribute this code for any purpose (include * commercial use) and without fee. Please refer to this package when you modify * this code. */ -------------------- License notice for sql sqrt floor -------------------- /* * Written by Wilco Dijkstra, 1996. The following email exchange establishes the * license. * * From: Wilco Dijkstra * Date: Fri, Jun 24, 2011 at 3:20 AM * Subject: Re: sqrt routine * To: Kevin Ma * Hi Kevin, * Thanks for asking. Those routines are public domain (originally posted to * comp.sys.arm a long time ago), so you can use them freely for any purpose. * Cheers, * Wilco * * ----- Original Message ----- * From: "Kevin Ma" * To: * Sent: Thursday, June 23, 2011 11:44 PM * Subject: Fwd: sqrt routine * Hi Wilco, * I saw your sqrt routine from several web sites, including * http://www.finesse.demon.co.uk/steven/sqrt.html. * Just wonder if there's any copyright information with your Successive * approximation routines, or if I can freely use it for any purpose. * Thanks. * Kevin */ -------------------- License notice for C++ Signal/Slot Library -------------------- // sigslot.h: Signal/Slot classes // // Written by Sarah Thompson (sarah@telergy.com) 2002. // // License: Public domain. You are free to use this code however you like, with // the proviso that the author takes on no responsibility or liability for any // use. -------------------- License notice for Wuffs (Wrangling Untrusted File Formats Safely) -------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "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. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "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. "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). "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. "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." "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. 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. 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. 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: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (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 (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. 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. 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. 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. 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. 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. 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To 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. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------- License notice for zlib -------------------- version 1.2.12, March 27th, 2022 Copyright (C) 1995-2022 Jean-loup Gailly and Mark Adler This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. -------------------- License notice for Zstandard -------------------- BSD License For Zstandard software Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Facebook, nor Meta, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: api/selenium/driver/chromedriver2 ================================================ [File too large to display: 17.2 MB] ================================================ FILE: api/selenium/server/selenium-server.jar ================================================ [File too large to display: 36.4 MB] ================================================ FILE: api/src/config.ts ================================================ import { FastifyServerOptions } from "fastify"; import { env } from "./env.js"; import stringify from "json-stringify-safe"; interface LoggingConfig { [key: string]: FastifyServerOptions["logger"]; } export const loggingConfig: LoggingConfig = { development: { transport: { target: "pino-pretty", options: { translateTime: "HH:MM:ss Z", ignore: "pid,hostname", }, }, ...(env.ENABLE_VERBOSE_LOGGING ? { hooks: { logMethod(inputArgs: any[], method: any) { if (inputArgs.length > 1) { try { let resultingMessage = ""; if (typeof inputArgs[0] === "string") { const [message, ...args] = inputArgs; resultingMessage = `${message} ${stringify(args)}`; } else { resultingMessage = stringify(inputArgs); } return method.apply(this, [resultingMessage]); } catch (error) { console.error( "Error trying to process logs with verbose logging enabled: ", error, ); } } return method.apply(this, inputArgs); }, }, } : {}), level: process.env.LOG_LEVEL || "debug", }, production: {}, test: false, }; ================================================ FILE: api/src/env.ts ================================================ import { z } from "zod"; import { config } from "dotenv"; config(); const envSchema = z.object({ NODE_ENV: z .enum(["test", "development", "staging", "production", "preview"]) .default("development"), HOST: z.string().optional().default("0.0.0.0"), DOMAIN: z.string().optional(), PORT: z.string().optional().default("3000"), USE_SSL: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("false"), CDP_REDIRECT_PORT: z.string().optional().default("9222"), CDP_DOMAIN: z.string().optional(), PROXY_URL: z.string().optional(), DEFAULT_HEADERS: z .string() .optional() .transform((val) => (val ? JSON.parse(val) : {})) .pipe(z.record(z.string()).optional().default({})), KILL_TIMEOUT: z.string().optional().default("0"), CHROME_EXECUTABLE_PATH: z.string().optional(), CHROME_HEADLESS: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("true"), DISPLAY: z.string().optional().default(":10"), ENABLE_CDP_LOGGING: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("false"), LOG_CUSTOM_EMIT_EVENTS: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("false"), ENABLE_VERBOSE_LOGGING: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("false"), DEFAULT_TIMEZONE: z.string().optional(), TIMEZONE_SERVICE_URL: z.string().optional(), SKIP_FINGERPRINT_INJECTION: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("false"), CHROME_ARGS: z .string() .optional() .transform((val) => (val ? val.split(" ").map((arg) => arg.trim()) : [])) .default(""), FILTER_CHROME_ARGS: z .string() .optional() .transform((val) => (val ? val.split(" ").map((arg) => arg.trim()) : [])) .default(""), DEBUG_CHROME_PROCESS: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("false"), PROXY_INTERNAL_BYPASS: z.string().optional(), CHROME_USER_DATA_DIR: z.string().optional(), LOG_STORAGE_ENABLED: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("false"), LOG_STORAGE_PATH: z.string().optional(), DISABLE_CHROME_SANDBOX: z .string() .optional() .transform((val) => val === "true" || val === "1") .default("false"), }); export const env = envSchema.parse(process.env); ================================================ FILE: api/src/index.ts ================================================ import fastify from "fastify"; import fastifyCors from "@fastify/cors"; import fastifySensible from "@fastify/sensible"; import steelBrowserPlugin from "./steel-browser-plugin.js"; import uiPlugin from "./plugins/ui-plugin.js"; import { loggingConfig } from "./config.js"; import { MB } from "./utils/size.js"; import path from "node:path"; const HOST = process.env.HOST ?? "0.0.0.0"; const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; export const server = fastify({ logger: loggingConfig[process.env.NODE_ENV ?? "development"] ?? true, trustProxy: true, bodyLimit: 100 * MB, disableRequestLogging: true, }); const setupServer = async () => { await server.register(fastifySensible); await server.register(fastifyCors, { origin: true }); // Register UI plugin only in production (when we have built UI files) if (process.env.NODE_ENV === "production") { await server.register(uiPlugin, { uiDistPath: path.join(process.cwd(), "ui/dist"), uiPrefix: "/ui", }); } await server.register(steelBrowserPlugin, { fileStorage: { maxSizePerSession: 100 * MB, }, }); }; const startServer = async () => { try { await setupServer(); await server.listen({ port: PORT, host: HOST }); } catch (err) { server.log.error(err); process.exit(1); } }; startServer(); ================================================ FILE: api/src/modules/actions/actions.controller.ts ================================================ import { FastifyReply } from "fastify"; import { BrowserContext, Page, HTTPResponse } from "puppeteer-core"; import { CDPService } from "../../services/cdp/cdp.service.js"; import { SessionService } from "../../services/session.service.js"; import { ScrapeFormat } from "../../types/index.js"; import { getErrors } from "../../utils/errors.js"; import { updateLog } from "../../utils/logging.js"; import { IProxyServer } from "../../utils/proxy.js"; import { cleanHtml, getDefuddleContent, htmlToMarkdown, transformHtml, } from "../../utils/scrape/index.js"; import { normalizeUrl } from "../../utils/url.js"; import { PDFRequest, ScrapeRequest, ScreenshotRequest, SearchRequest } from "./actions.schema.js"; import { DefuddleResponse } from "defuddle"; import pdf2html from "pdf2html"; import { buildHtmlLikeMetadataFromPdf, extractLinksFromConvertedHtml, } from "../../utils/scrape/pdfToHtml.js"; import { safeGoto } from "../../utils/scrape/safeGoTo.js"; export const handleScrape = async ( sessionService: SessionService, browserService: CDPService, request: ScrapeRequest, reply: FastifyReply, ) => { const startTime = Date.now(); let times: Record = {}; const { url, format, screenshot, pdf, proxyUrl, logUrl, delay } = request.body; let proxy: IProxyServer | null = null; let context: BrowserContext | null = null; try { if (proxyUrl) { proxy = await sessionService.proxyFactory(proxyUrl); await proxy.listen(); } times.proxyTime = Date.now() - startTime; let page: Page; let response: HTTPResponse | null = null; let pdfResponse: HTTPResponse | null = null; let isPdfNavigation = false; if (!browserService.isRunning()) { await browserService.launch(); } if (proxy) { // If a proxy is used, we proceed with browser navigation; implementing proxy-aware Node fetch // would require an HTTP agent and is outside current scope. context = await browserService.createBrowserContext(proxy.url); page = await context.newPage(); times.proxyPageTime = Date.now() - startTime - times.proxyTime; } else { page = await browserService.getPrimaryPage(); times.pageTime = Date.now() - startTime - times.proxyTime; } // PDF retrieval will use node fetch with session cookies; removed CDP tracking let normalizedUrl: string | null = null; if (url) { normalizedUrl = normalizeUrl(url); if (!normalizedUrl) { throw new Error(`Invalid URL: ${url}`); } } const safeResponse = normalizedUrl ? await safeGoto(page, normalizedUrl, { timeout: 30000, waitUntil: "domcontentloaded", }) : { response: null, isPdf: false, pdfResponse: null }; response = safeResponse.response !== null ? safeResponse.response : safeResponse.pdfResponse; pdfResponse = safeResponse.pdfResponse; const isPdf = safeResponse.isPdf; if (delay) { await new Promise((resolve) => setTimeout(resolve, delay)); } const contentType = response?.headers()["content-type"]?.toLowerCase() || ""; let scrapeResponse: Record = {}; let htmlContent = ""; let cleanedHtml: string; let readabilityContent: DefuddleResponse; if (isPdf || contentType.includes("application/pdf")) { // Node fetch using session cookies (same browser auth state) const targetUrl = normalizedUrl || url!; const cookies = await page.cookies(targetUrl); const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join("; "); const fetchHeaders: Record = {}; if (cookieHeader) fetchHeaders["Cookie"] = cookieHeader; if (!fetchHeaders["Referer"]) { const u = new URL(targetUrl); fetchHeaders["Referer"] = u.origin + "/"; } const nodeRes = await fetch(targetUrl, { method: "GET", redirect: "follow", headers: fetchHeaders, }); const nodeCT = (nodeRes.headers.get("content-type") || "").toLowerCase(); if (!nodeRes.ok || !nodeCT.includes("application/pdf")) { throw new Error(`Expected PDF; got status ${nodeRes.status} content-type ${nodeCT}`); } const arrBuf = await nodeRes.arrayBuffer(); const pdfBuffer = Buffer.from(arrBuf); const convertStart = Date.now(); htmlContent = await pdf2html.html(pdfBuffer); times.pdfHtmlConvertTime = Date.now() - convertStart; const metaStart = Date.now(); const pdfMeta = await pdf2html.meta(pdfBuffer); times.pdfMetaTime = Date.now() - metaStart; const htmlMeta = buildHtmlLikeMetadataFromPdf(pdfMeta, { urlSource: targetUrl, statusCode: nodeRes.status, htmlForFallback: htmlContent, }); const htmlLinks = extractLinksFromConvertedHtml(htmlContent); scrapeResponse = { content: {}, metadata: { ...htmlMeta, statusCode: nodeRes.status, headers: Object.fromEntries(nodeRes.headers.entries()), originalContentType: nodeCT, pdfAcquisition: "node-fetch-with-cookies", }, links: htmlLinks, }; if (pdf) { scrapeResponse.pdf = pdfBuffer.toString("base64"); } } else { // Regular HTML flow await page.evaluate(() => { (window as any).__name = (func: Function) => func; }); const [{ html, metadata, links }, base64Screenshot, pdfBuffer] = await Promise.all([ page.evaluate(() => { const getMetaContent = (selector: string) => { const element = document.querySelector(selector); return element ? element.getAttribute("content") : null; }; const getMetaByName = (name: string) => getMetaContent(`meta[name="${name}"]`); const getMetaByProperty = (property: string) => getMetaContent(`meta[property="${property}"]`); const extractJsonLd = () => { const scripts = document.querySelectorAll('script[type="application/ld+json"]'); const jsonLdData: any[] = []; scripts.forEach((script) => { try { const data = JSON.parse(script.textContent || ""); jsonLdData.push(data); } catch (e) { console.error(e); } }); return jsonLdData; }; return { html: document.documentElement.outerHTML, links: [...document.links].map((l) => ({ url: l.href, text: l.textContent?.trim() || "", })), metadata: { title: document.title, language: document.documentElement.lang, urlSource: window.location.href, timestamp: new Date().toISOString(), description: getMetaByName("description"), keywords: getMetaByName("keywords"), author: getMetaByName("author"), ogTitle: getMetaByProperty("og:title"), ogDescription: getMetaByProperty("og:description"), ogImage: getMetaByProperty("og:image"), ogUrl: getMetaByProperty("og:url"), ogSiteName: getMetaByProperty("og:site_name"), articleAuthor: getMetaByProperty("article:author"), publishedTime: getMetaByProperty("article:published_time"), modifiedTime: getMetaByProperty("article:modified_time"), canonical: document.querySelector('link[rel="canonical"]')?.getAttribute("href"), favicon: document.querySelector('link[rel="icon"]')?.getAttribute("href"), jsonLd: extractJsonLd(), statusCode: 200, }, }; }), screenshot ? page.screenshot({ encoding: "base64", type: "jpeg", quality: 100 }) : null, pdf ? page.pdf() : null, ]); htmlContent = html; times.extractionTime = Date.now() - startTime - (times.pageLoadTime || 0); scrapeResponse = { content: {}, metadata, links }; if (base64Screenshot) { scrapeResponse.screenshot = base64Screenshot; } if (pdfBuffer) { scrapeResponse.pdf = Buffer.from(pdfBuffer).toString("base64"); } } // Format handling (works for both PDF converted HTML and normal HTML) if (format && format.length > 0) { if (format.includes(ScrapeFormat.HTML)) { scrapeResponse.content.html = htmlContent; } const needsCleanedHtml = format.includes(ScrapeFormat.CLEANED_HTML); const needsReadability = format.includes(ScrapeFormat.READABILITY) || format.includes(ScrapeFormat.MARKDOWN); if (needsCleanedHtml) { const cleanHtmlStart = Date.now(); cleanedHtml = cleanHtml(htmlContent); times.cleanedHtmlTime = Date.now() - cleanHtmlStart; if (format.includes(ScrapeFormat.CLEANED_HTML)) { scrapeResponse.content.cleaned_html = cleanedHtml; } } if (needsReadability) { const readabilityStart = Date.now(); readabilityContent = await getDefuddleContent( transformHtml(htmlContent, normalizedUrl || url), ); times.readabilityTime = Date.now() - readabilityStart; if (format.includes(ScrapeFormat.READABILITY)) { scrapeResponse.content.readability = readabilityContent.content; } } if (format.includes(ScrapeFormat.MARKDOWN)) { const markdownStart = Date.now(); scrapeResponse.content.markdown = await htmlToMarkdown(readabilityContent!.content); times.markdownTime = Date.now() - markdownStart; } } else { scrapeResponse.content.html = htmlContent; } times.totalInstanceTime = Date.now() - startTime; if (logUrl) { await updateLog(logUrl, { times }); } return reply.send(scrapeResponse); } catch (e: unknown) { const error = getErrors(e); if (logUrl) { await updateLog(logUrl, { times, response: { browserError: error } }); } if (url) { await browserService.refreshPrimaryPage(); } return reply.code(500).send({ message: error }); } finally { if (context) { await context.close().catch(() => {}); } if (proxy) { await proxy.close(true).catch(() => {}); } } }; export const handleSearch = async ( sessionService: SessionService, browserService: CDPService, request: SearchRequest, reply: FastifyReply, ) => { const startTime = Date.now(); let times: Record = {}; const { query, proxyUrl, logUrl } = request.body; let proxy: IProxyServer | null = null; let context: BrowserContext | null = null; try { if (proxyUrl) { proxy = await sessionService.proxyFactory(proxyUrl); await proxy.listen(); } times.proxyTime = Date.now() - startTime; let page: Page; if (!browserService.isRunning()) { await browserService.launch(); } if (proxy) { // If a proxy is used, we proceed with browser navigation; implementing proxy-aware Node fetch // would require an HTTP agent and is outside current scope. context = await browserService.createBrowserContext(proxy.url); page = await context.newPage(); times.proxyPageTime = Date.now() - startTime - times.proxyTime; } else { page = await browserService.getPrimaryPage(); times.pageTime = Date.now() - startTime - times.proxyTime; } await page.evaluate(() => { (window as any).__name = (func: Function) => func; }); // Go to Brave await page.goto(`https://search.brave.com/search?q=${encodeURIComponent(query)}`, { waitUntil: "networkidle2", }); // Wait for results to load await page.waitForSelector("#results"); // Scrape results const results = await page.evaluate(() => { const items = document.querySelectorAll("div.snippet"); return Array.from(items) .map((item) => { if ( [ "llm-snippet", "faq", "pagination-snippet", "search-elsewhere", "infoblox-snippet", "discussions", ].includes(item.id) ) { return; } const urlEl = item.querySelector("div.result-content a"); const descEl = item.querySelector("div.generic-snippet"); const titleEl = item.querySelector("div.result-content a div.title"); return { title: titleEl?.textContent?.trim() || null, url: urlEl?.getAttribute("href") || null, description: descEl?.textContent?.split("-")[1]?.trim() || null, }; }) .filter( (item) => item && typeof item === "object" && "title" in item && "url" in item && "description" in item && item.title !== null && item.url !== null, ); }); times.totalInstanceTime = Date.now() - startTime; if (logUrl) { await updateLog(logUrl, { times }); } return reply.send({ results }); } catch (e: unknown) { const error = getErrors(e); if (logUrl) { await updateLog(logUrl, { times, response: { browserError: error } }); } return reply.code(500).send({ message: error }); } finally { if (context) { await context.close().catch(() => {}); } if (proxy) { await proxy.close(true).catch(() => {}); } } }; export const handleScreenshot = async ( sessionService: SessionService, browserService: CDPService, request: ScreenshotRequest, reply: FastifyReply, ) => { const startTime = Date.now(); let times: Record = {}; const { url, logUrl, proxyUrl, delay, fullPage } = request.body; let proxy: IProxyServer | null = null; let context: BrowserContext | null = null; if (!browserService.isRunning()) { await browserService.launch(); } try { if (proxyUrl) { proxy = await sessionService.proxyFactory(proxyUrl); await proxy.listen(); } times.proxyTime = Date.now() - startTime; let page: Page; if (proxy) { context = await browserService.createBrowserContext(proxy.url); page = await context.newPage(); times.proxyPageTime = Date.now() - startTime - times.proxyTime; } else { page = await browserService.getPrimaryPage(); times.pageTime = Date.now() - startTime; } if (url) { const normalizedUrl = normalizeUrl(url); if (!normalizedUrl) { throw new Error(`Invalid URL: ${url}`); } await page.goto(normalizedUrl, { timeout: 30000, waitUntil: "domcontentloaded" }); times.pageLoadTime = Date.now() - times.pageTime - times.proxyTime - startTime; } if (delay) { await new Promise((resolve) => setTimeout(resolve, delay)); } const screenshot = await page.screenshot({ fullPage, type: "jpeg", quality: 100 }); times.screenshotTime = Date.now() - times.pageLoadTime - times.pageTime - times.proxyTime - startTime; if (logUrl) { await updateLog(logUrl, { times }); } return reply.send(screenshot); } catch (e: unknown) { const error = getErrors(e); if (logUrl) { await updateLog(logUrl, { times, response: { browserError: error } }); } if (url) { await browserService.refreshPrimaryPage(); } return reply.code(500).send({ message: error }); } finally { if (context) { await context.close().catch(() => {}); } if (proxy) { await proxy.close(true).catch(() => {}); } } }; export const handlePDF = async ( sessionService: SessionService, browserService: CDPService, request: PDFRequest, reply: FastifyReply, ) => { const startTime = Date.now(); let times: Record = {}; const { url, logUrl, proxyUrl, delay } = request.body; let proxy: IProxyServer | null = null; let context: BrowserContext | null = null; if (!browserService.isRunning()) { await browserService.launch(); } try { if (proxyUrl) { proxy = await sessionService.proxyFactory(proxyUrl); await proxy.listen(); } times.proxyTime = Date.now() - startTime; let page: Page; if (proxy) { context = await browserService.createBrowserContext(proxy.url); page = await context.newPage(); times.proxyPageTime = Date.now() - startTime - times.proxyTime; } else { page = await browserService.getPrimaryPage(); times.pageTime = Date.now() - startTime; } if (url) { const normalizedUrl = normalizeUrl(url); if (!normalizedUrl) { throw new Error(`Invalid URL: ${url}`); } await page.goto(normalizedUrl, { timeout: 30000, waitUntil: "domcontentloaded" }); times.pageLoadTime = Date.now() - times.pageTime - times.proxyTime - startTime; } if (delay) { await new Promise((resolve) => setTimeout(resolve, delay)); } const pdf = await page.pdf(); times.pdfTime = Date.now() - times.pageLoadTime - times.pageTime - times.proxyTime - startTime; if (logUrl) { await updateLog(logUrl, { times }); } return reply.send(pdf); } catch (e: unknown) { const error = getErrors(e); if (logUrl) { await updateLog(logUrl, { times, response: { browserError: error } }); } if (url) { await browserService.refreshPrimaryPage(); } return reply.code(500).send({ message: error }); } finally { if (context) { await context.close().catch(() => {}); } if (proxy) { await proxy.close(true).catch(() => {}); } } }; ================================================ FILE: api/src/modules/actions/actions.routes.ts ================================================ import { FastifyInstance, FastifyReply } from "fastify"; import { handlePDF, handleScrape, handleScreenshot, handleSearch } from "./actions.controller.js"; import { $ref } from "../../plugins/schemas.js"; import { PDFRequest, ScrapeRequest, ScreenshotRequest, SearchRequest } from "./actions.schema.js"; async function routes(server: FastifyInstance) { server.post( "/scrape", { schema: { operationId: "scrape", description: "Scrape a URL", tags: ["Browser Actions"], summary: "Scrape a URL", body: $ref("ScrapeRequest"), response: { 200: $ref("ScrapeResponse"), }, }, }, async (request: ScrapeRequest, reply: FastifyReply) => handleScrape(server.sessionService, server.cdpService, request, reply), ); server.post( "/screenshot", { schema: { operationId: "screenshot", description: "Take a screenshot", tags: ["Browser Actions"], summary: "Take a screenshot", body: $ref("ScreenshotRequest"), response: { 200: $ref("ScreenshotResponse"), }, }, }, async (request: ScreenshotRequest, reply: FastifyReply) => handleScreenshot(server.sessionService, server.cdpService, request, reply), ); server.post( "/pdf", { schema: { operationId: "pdf", description: "Get the PDF content of a page", tags: ["Browser Actions"], summary: "Get the PDF content of a page", body: $ref("PDFRequest"), response: { 200: $ref("PDFResponse"), }, }, }, async (request: PDFRequest, reply: FastifyReply) => handlePDF(server.sessionService, server.cdpService, request, reply), ); server.post( "/search", { schema: { operationId: "search", description: "Use a search engine to search for URLs given a query", tags: ["Browser Actions"], summary: "Use a search engine to search for URLs given a query", body: $ref("SearchRequest"), response: { 200: $ref("SearchResponse"), }, }, }, async (request: SearchRequest, reply: FastifyReply) => handleSearch(server.sessionService, server.cdpService, request, reply), ); } export default routes; ================================================ FILE: api/src/modules/actions/actions.schema.ts ================================================ import { FastifyRequest } from "fastify"; import { z } from "zod"; import { ScrapeFormat } from "../../types/enums.js"; const ScrapeRequest = z.object({ url: z.string().optional(), format: z.array(z.nativeEnum(ScrapeFormat)).optional(), screenshot: z.boolean().optional(), pdf: z.boolean().optional(), proxyUrl: z .string() .nullable() .optional() .describe( "Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.", ), delay: z.number().optional(), logUrl: z.string().optional(), }); const ScrapeResponse = z.object({ content: z.record(z.nativeEnum(ScrapeFormat), z.any()), metadata: z.object({ title: z.string().optional(), language: z.string().optional(), urlSource: z.string().optional(), timestamp: z.string().datetime().optional(), description: z.string().optional(), keywords: z.string().optional(), author: z.string().optional(), ogTitle: z.string().optional(), ogDescription: z.string().optional(), ogImage: z.string().optional(), ogUrl: z.string().optional(), ogSiteName: z.string().optional(), articleAuthor: z.string().optional(), publishedTime: z.string().optional(), modifiedTime: z.string().optional(), canonical: z.string().optional(), favicon: z.string().optional(), jsonLd: z.any().optional(), statusCode: z.number().int(), }), links: z.array( z.object({ url: z.string(), text: z.string(), }), ), screenshot: z.string().optional(), pdf: z.string().optional(), }); const ScreenshotRequest = z.object({ url: z.string().optional(), proxyUrl: z .string() .nullable() .optional() .describe( "Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.", ), delay: z.number().optional(), fullPage: z.boolean().optional(), logUrl: z.string().optional(), }); const ScreenshotResponse = z.any(); const PDFRequest = z.object({ url: z.string().optional(), proxyUrl: z .string() .nullable() .optional() .describe( "Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.", ), delay: z.number().optional(), logUrl: z.string().optional(), }); const SearchRequest = z.object({ query: z.string(), proxyUrl: z .string() .nullable() .optional() .describe( "Proxy URL to use for the scrape. Provide `null` to disable proxy. If not provided, current session proxy settings will be used.", ), logUrl: z.string().optional(), }); const SearchResponse = z.object({ results: z.array( z.object({ title: z.string(), url: z.string(), description: z.string().nullable().optional(), }), ), }); const PDFResponse = z.any(); export type ScrapeRequestBody = z.infer; export type ScrapeRequest = FastifyRequest<{ Body: ScrapeRequestBody }>; export type ScreenshotRequestBody = z.infer; export type ScreenshotRequest = FastifyRequest<{ Body: ScreenshotRequestBody }>; export type PDFRequestBody = z.infer; export type PDFRequest = FastifyRequest<{ Body: PDFRequestBody }>; export type SearchRequestBody = z.infer; export type SearchRequest = FastifyRequest<{ Body: SearchRequestBody }>; export const actionsSchemas = { ScrapeRequest, ScrapeResponse, ScreenshotRequest, ScreenshotResponse, PDFRequest, PDFResponse, SearchRequest, SearchResponse, }; export default actionsSchemas; ================================================ FILE: api/src/modules/cdp/cdp.routes.ts ================================================ import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; import { z } from "zod"; import { $ref } from "../../plugins/schemas.js"; import cdpSchemas from "./cdp.schemas.js"; async function routes(server: FastifyInstance) { server.get( "/devtools/inspector.html", { schema: { operationId: "getDevtoolsUrl", description: "Get the URL for the DevTools inspector", tags: ["CDP"], summary: "Get the URL for the DevTools inspector", querystring: $ref("GetDevtoolsUrlSchema"), }, }, async ( request: FastifyRequest<{ Querystring: z.infer }>, reply: FastifyReply, ) => { return reply.redirect( `${server.cdpService.getDebuggerUrl()}?ws=${server.cdpService .getDebuggerWsUrl(request.query.pageId) .replace("ws:", "")}`, ); }, ); } export default routes; ================================================ FILE: api/src/modules/cdp/cdp.schemas.ts ================================================ import { z } from "zod"; export const GetDevtoolsUrlSchema = z.object({ pageId: z.string().optional(), }); export default { GetDevtoolsUrlSchema, }; ================================================ FILE: api/src/modules/files/files.controller.ts ================================================ import { MultipartFile } from "@fastify/multipart"; import archiver from "archiver"; import { randomUUID } from "crypto"; import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import * as fs from "fs"; import http from "http"; import https from "https"; import mime from "mime-types"; import { tmpdir } from "os"; import path from "path"; import { Readable } from "stream"; import { pipeline } from "stream/promises"; import { v4 as uuidv4 } from "uuid"; import { FileService } from "../../services/file.service.js"; import { getErrors } from "../../utils/errors.js"; export class FilesController { constructor(private fileService: FileService) {} private validatePath(filePath: string): boolean { if (path.isAbsolute(filePath)) { return false; } if (filePath.includes("..")) { return false; } if (filePath.includes("\0")) { return false; } const normalized = path.normalize(filePath); if (normalized.startsWith("..")) { return false; } return true; } async handleFileUpload( server: FastifyInstance, request: FastifyRequest<{ Params: { sessionId: string } }>, reply: FastifyReply, ) { let tempFilePath: string | null = null; try { if (!request.isMultipart()) { return reply.code(400).send({ success: false, message: "Request must be multipart/form-data", }); } let filePath: string | null = null; let fileUrl: string | null = null; let fileProvided: boolean = false; let saveFileResult: Awaited> | null = null; for await (const part of request.parts()) { if (part.fieldname === "file") { if (part.type === "file") { const file = part as MultipartFile; fileProvided = true; tempFilePath = path.join(tmpdir(), `upload_${uuidv4()}`); const writeStream = fs.createWriteStream(tempFilePath); await pipeline(file.file, writeStream); } else if (part.type === "field" && typeof part.value === "string") { fileUrl = part.value; } } else if ( part.fieldname === "path" && part.type === "field" && typeof part.value === "string" ) { filePath = part.value; } } if (!fileProvided && !fileUrl) { return reply.code(400).send({ success: false, message: "No file provided in the multipart request. The 'file' field must contain either a file or a URL string.", }); } let finalPath: string; if (fileProvided && tempFilePath) { if (!filePath) { finalPath = randomUUID(); } else { if (!this.validatePath(filePath)) { await fs.promises.unlink(tempFilePath).catch(() => {}); return reply.code(400).send({ success: false, message: "Invalid path provided", }); } finalPath = filePath; } const readStream = fs.createReadStream(tempFilePath); saveFileResult = await this.fileService.saveFile({ filePath: finalPath, stream: readStream, }); await fs.promises.unlink(tempFilePath).catch(() => {}); tempFilePath = null; } else if (fileUrl) { if (!filePath) { const urlPath = new URL(fileUrl).pathname; const filename = urlPath.split("/").pop() || randomUUID(); const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, "_"); finalPath = sanitizedFilename; } else { if (!this.validatePath(filePath)) { return reply.code(400).send({ success: false, message: "Invalid path provided", }); } finalPath = filePath; } const { stream } = await this.createStreamFromUrl(fileUrl); saveFileResult = await this.fileService.saveFile({ filePath: finalPath, stream, }); } if (!saveFileResult) { return reply.code(500).send({ success: false, message: "Failed to save file", }); } return reply.send({ path: saveFileResult.path, size: saveFileResult.size, lastModified: saveFileResult.lastModified, }); } catch (e: unknown) { if (tempFilePath) { await fs.promises.unlink(tempFilePath).catch(() => {}); } const error = getErrors(e); return reply.code(500).send({ success: false, message: error }); } } private async createStreamFromUrl( url: string, ): Promise<{ stream: Readable; contentType?: string; name: string }> { return new Promise((resolve, reject) => { const protocol = url.startsWith("https") ? https : http; protocol .get(url, (response) => { if (response.statusCode !== 200) { return reject(new Error(`Failed to fetch file: ${response.statusCode}`)); } const contentType = response.headers["content-type"]; const disposition = response.headers["content-disposition"] || ""; let name: string | null = null; const nameMatch = disposition.match(/filename="(.+)"/i); if (nameMatch && nameMatch[1]) { name = nameMatch[1]; } else { name = url.split("/").pop() || "downloaded-file"; } resolve({ stream: response, contentType, name, }); }) .on("error", reject); }); } async handleFileDownload( server: FastifyInstance, request: FastifyRequest<{ Params: { sessionId: string; "*": string } }>, reply: FastifyReply, ) { try { const { stream, size, lastModified } = await this.fileService.downloadFile({ filePath: request.params["*"], }); const name = request.params["*"].split("/").pop() || "downloaded-file"; reply .header("Content-Type", mime.lookup(request.params["*"]) || "application/octet-stream") .header("Content-Length", size) .header("Content-Disposition", `attachment; filename="${encodeURIComponent(name)}"`) .header("Last-Modified", lastModified.toISOString()); return reply.send(stream); } catch (e: unknown) { const error = getErrors(e); return reply.code(500).send({ success: false, message: error }); } } async handleFileHead( server: FastifyInstance, request: FastifyRequest<{ Params: { sessionId: string; "*": string } }>, reply: FastifyReply, ) { const { size, lastModified } = await this.fileService.getFile({ filePath: request.params["*"], }); const name = request.params["*"].split("/").pop() || "downloaded-file"; reply .header("Content-Length", size) .header("Last-Modified", lastModified.toISOString()) .header("Content-Type", mime.lookup(request.params["*"]) || "application/octet-stream") .header("Content-Disposition", `attachment; filename="${encodeURIComponent(name)}"`); return reply.code(200).send(); } async handleFileList( server: FastifyInstance, request: FastifyRequest<{ Params: { sessionId: string }; }>, reply: FastifyReply, ) { try { const files = await this.fileService.listFiles(); return reply.send({ data: files.map((file) => ({ path: file.path, size: file.size, lastModified: file.lastModified, })), }); } catch (e: unknown) { const error = getErrors(e); return reply.code(500).send({ success: false, message: error }); } } async handleFileDelete( server: FastifyInstance, request: FastifyRequest<{ Params: { sessionId: string; "*": string } }>, reply: FastifyReply, ) { try { await this.fileService.deleteFile({ filePath: request.params["*"], }); return reply.code(204).send(); } catch (e: unknown) { const error = getErrors(e); return reply.code(500).send({ success: false, message: error }); } } async handleFileDeleteAll( server: FastifyInstance, request: FastifyRequest<{ Params: { sessionId: string } }>, reply: FastifyReply, ) { try { await this.fileService.cleanupFiles(); return reply.code(204).send(); } catch (e: unknown) { const error = getErrors(e); return reply.code(500).send({ success: false, message: error }); } } async handleDownloadArchive( server: FastifyInstance, request: FastifyRequest<{ Params: { sessionId: string } }>, reply: FastifyReply, ) { const prebuiltArchivePath = await this.fileService.getPrebuiltArchivePath(); try { const stats = await fs.promises.stat(prebuiltArchivePath); if (stats.isFile()) { server.log.info(`Serving prebuilt archive: ${prebuiltArchivePath}`); const stream = fs.createReadStream(prebuiltArchivePath); reply.header("Content-Type", "application/zip"); reply.header("Content-Disposition", `attachment; filename="files.zip"`); reply.header("Content-Length", stats.size); reply.header("Last-Modified", stats.mtime.toUTCString()); return reply.send(stream); } else { server.log.warn(`Prebuilt archive path exists but is not a file: ${prebuiltArchivePath}`); } server.log.info("Sending empty archive."); reply.header("Content-Type", "application/zip"); reply.header("Content-Disposition", `attachment; filename="files-archive-empty.zip"`); const emptyArchive = archiver("zip", { zlib: { level: 9 } }); emptyArchive.pipe(reply.raw); await emptyArchive.finalize(); return; } catch (err: any) { server.log.error({ err }, "Error during handleFileArchive"); if (!reply.sent) { try { reply.code(500).send({ message: "Failed to process archive request" }); } catch (sendError: unknown) { server.log.error( { err: sendError }, "Error sending 500 response after archive handling error", ); } } } } } ================================================ FILE: api/src/modules/files/files.routes.ts ================================================ import fastifyMultipart from "@fastify/multipart"; import { FastifyInstance, FastifyRequest } from "fastify"; import { $ref } from "../../plugins/schemas.js"; import { FileService } from "../../services/file.service.js"; import { MB } from "../../utils/size.js"; import { FilesController } from "./files.controller.js"; async function routes(server: FastifyInstance) { const filesController = new FilesController(FileService.getInstance()); await server.register(fastifyMultipart, { limits: { fileSize: server.steelBrowserConfig.fileStorage?.maxSizePerSession ?? 100 * MB, }, attachFieldsToBody: false, }); server.post( "/sessions/:sessionId/files", { schema: { operationId: "upload_file", summary: "Upload a file", 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.", tags: ["Files"], consumes: ["multipart/form-data"], body: $ref("FileUploadRequest"), response: { 200: $ref("FileDetails"), }, }, validatorCompiler: () => (value) => ({ value }), }, async ( request: FastifyRequest<{ Params: { sessionId: string }; }>, reply, ) => filesController.handleFileUpload(server, request, reply), ); server.head( "/sessions/:sessionId/files/*", { schema: { operationId: "head_file", summary: "Head a file", description: "Head a file from a session", tags: ["Files"], }, }, async (request: FastifyRequest<{ Params: { sessionId: string; "*": string } }>, reply) => filesController.handleFileHead(server, request, reply), ); server.get( "/sessions/:sessionId/files/*", { schema: { operationId: "download_file", summary: "Download a file", description: "Download a file from a session", tags: ["Files"], }, }, async (request: FastifyRequest<{ Params: { sessionId: string; "*": string } }>, reply) => filesController.handleFileDownload(server, request, reply), ); server.get( "/sessions/:sessionId/files", { schema: { operationId: "list_files", summary: "List files", description: "List all files from the session in descending order.", tags: ["Files"], response: { 200: $ref("MultipleFiles"), }, }, }, async ( request: FastifyRequest<{ Params: { sessionId: string }; }>, reply, ) => filesController.handleFileList(server, request, reply), ); server.delete( "/sessions/:sessionId/files/*", { schema: { operationId: "delete_file", summary: "Delete a file", description: "Delete a file from a session", tags: ["Files"], response: { 204: { type: "null", description: "No content", }, }, }, }, async (request: FastifyRequest<{ Params: { sessionId: string; "*": string } }>, reply) => filesController.handleFileDelete(server, request, reply), ); server.delete( "/sessions/:sessionId/files", { schema: { operationId: "delete_all_files", summary: "Delete all files", description: "Delete all files from a session", tags: ["Files"], response: { 204: { type: "null", description: "No content", }, }, }, }, async (request: FastifyRequest<{ Params: { sessionId: string } }>, reply) => filesController.handleFileDeleteAll(server, request, reply), ); server.get( "/sessions/:sessionId/files.zip", { schema: { operationId: "download_archive", summary: "Download archive", description: "Download all files from the session as a zip archive.", tags: ["Files"], }, }, async (request: FastifyRequest<{ Params: { sessionId: string } }>, reply) => filesController.handleDownloadArchive(server, request, reply), ); } export default routes; ================================================ FILE: api/src/modules/files/files.schema.ts ================================================ import { z } from "zod"; const FileUploadRequest = z.object({ file: z.any().describe("The file to upload (binary) or URL string to download from"), path: z.string().optional().describe("Path to the file in the storage system"), }); const FileDetails = z.object({ path: z.string().describe("Path to the file in the storage system"), size: z.number().describe("Size of the file in bytes"), lastModified: z.string().datetime().describe("Timestamp when the file was last updated"), }); const MultipleFiles = z.object({ data: z.array(FileDetails).describe("Array of files for the current page"), }); export type FileDetails = z.infer; export type MultipleFiles = z.infer; export type FileUploadRequest = z.infer; export const filesSchemas = { FileUploadRequest, FileDetails, MultipleFiles, }; export default filesSchemas; ================================================ FILE: api/src/modules/logs/logs.routes.ts ================================================ import { FastifyPluginAsync, FastifyRequest } from "fastify"; import { LogQuerySchema, ExportLogsSchema, LogQueryInput } from "./logs.schema.js"; import { randomUUID } from "crypto"; import { $ref } from "../../plugins/schemas.js"; import { LogQuery } from "../../services/cdp/instrumentation/storage/index.js"; import { EmitEvent } from "../../types/enums.js"; const logsRoutes: FastifyPluginAsync = async (fastify) => { const storage = fastify.cdpService.getInstrumentationLogger()?.getStorage?.(); if (!storage) { fastify.log.warn("Log storage not available. Logs routes will not work."); return; } /** * Query logs from local storage */ fastify.get( "/query", { schema: { querystring: $ref("LogQuerySchema"), tags: ["Logs"], description: "Query browser logs from local storage", }, }, async (request: FastifyRequest<{ Querystring: LogQueryInput }>) => { const query = request.query; let result; try { result = await storage.query({ startTime: query.startTime ? new Date(query.startTime) : undefined, endTime: query.endTime ? new Date(query.endTime) : undefined, eventTypes: query.eventTypes ? query.eventTypes.split(",") : undefined, pageId: query.pageId, targetType: query.targetType, limit: query.limit, offset: query.offset, }); } catch (error) { fastify.log.error({ error }, "Error querying logs"); throw error; } return result; }, ); /** * Get log statistics */ fastify.get( "/stats", { schema: { tags: ["Logs"], description: "Get statistics about stored browser logs", }, }, async () => { const stats = await storage.getStats(); return { totalEvents: stats.totalEvents, oldestEvent: stats.oldestEvent?.toISOString() || null, newestEvent: stats.newestEvent?.toISOString() || null, sizeBytes: stats.sizeBytes, }; }, ); /** * Stream logs in real-time using Server-Sent Events */ fastify.get( "/stream", { schema: { tags: ["Logs"], description: "Stream browser logs in real-time using SSE", }, }, async (request, reply) => { const logger = fastify.cdpService.getInstrumentationLogger(); if (!logger) { return reply.code(503).send({ error: "Browser logger not available" }); } // Set SSE headers reply.raw.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }); // Send initial comment to establish connection reply.raw.write(": connected\n\n"); // Listen for new log events const handleLog = (event: any, context: any) => { const data = JSON.stringify({ ...event, ...context }); reply.raw.write(`data: ${data}\n\n`); }; logger.on?.(EmitEvent.Log, handleLog); // Clean up on disconnect request.raw.on("close", () => { logger.off?.(EmitEvent.Log, handleLog); }); }, ); /** * Export logs to Parquet format */ fastify.post( "/export", { schema: { querystring: $ref("LogQuerySchema"), tags: ["Logs"], description: "Export browser logs to Parquet format", }, }, async (request: FastifyRequest<{ Querystring: LogQuery }>) => { const query = request.query; // Generate a unique filename const fileName = `steel-browser-logs-${randomUUID()}.parquet`; const filePath = `/tmp/steel-browser-exports/${fileName}`; // Export with optional query filters const exportedPath = await storage.exportToParquet(filePath, query); return { filePath: exportedPath, message: "Logs exported successfully", }; }, ); /** * Clear all logs from storage */ fastify.delete( "/", { schema: { tags: ["Logs"], description: "Clear all browser logs from storage", }, }, async () => { await storage.clear(); return { message: "Logs cleared successfully", }; }, ); }; export default logsRoutes; ================================================ FILE: api/src/modules/logs/logs.schema.ts ================================================ import { z } from "zod"; export const LogQuerySchema = z.object({ startTime: z.string().datetime().optional(), endTime: z.string().datetime().optional(), eventTypes: z.string().optional(), pageId: z.string().optional(), targetType: z.string().optional(), limit: z.number().int().min(1).max(1000).optional().default(100), offset: z.number().int().min(0).optional().default(0), }); export const LogStatsSchema = z.object({ totalEvents: z.number(), oldestEvent: z.string().datetime().nullable(), newestEvent: z.string().datetime().nullable(), sizeBytes: z.number(), }); export const LogQueryResultSchema = z.object({ events: z.array(z.record(z.any())), total: z.number(), hasMore: z.boolean(), }); export const ExportLogsSchema = z.object({ query: LogQuerySchema.optional(), }); export type LogQueryInput = z.infer; export type LogStatsOutput = z.infer; export type LogQueryResultOutput = z.infer; export type ExportLogsInput = z.infer; export const loggingSchemas = { LogQuerySchema, LogStatsSchema, LogQueryResultSchema, ExportLogsSchema, }; export default loggingSchemas; ================================================ FILE: api/src/modules/selenium/selenium.routes.ts ================================================ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import fastifyReplyFrom from "@fastify/reply-from"; async function routes(server: FastifyInstance) { server.register(fastifyReplyFrom, { base: server.seleniumService.getSeleniumServerUrl(), }); server.all( "/selenium/wd/*", { schema: { hide: true } }, async (request: FastifyRequest, reply: FastifyReply) => { if (request.url === "/selenium/wd/session" && request.method === "POST") { const body = request.body as any; if (!body.capabilities) { body.capabilities = {}; } if (!body.capabilities.alwaysMatch) { body.capabilities.alwaysMatch = {}; } if (!body.capabilities.alwaysMatch["goog:chromeOptions"]) { body.capabilities.alwaysMatch["goog:chromeOptions"] = {}; } if (!body.capabilities.alwaysMatch["goog:chromeOptions"].args) { body.capabilities.alwaysMatch["goog:chromeOptions"].args = []; } const chromeArgs = await server.seleniumService.getChromeArgs(); body.capabilities.alwaysMatch["goog:chromeOptions"].args.push(...chromeArgs); request.body = body; return reply.from("/session", { body, rewriteHeaders(headers, request) { headers["content-type"] = "application/json; charset=utf-8"; headers["accept"] = "application/json; charset=utf-8"; return headers; }, rewriteRequestHeaders(request, headers) { headers["content-type"] = "application/json; charset=utf-8"; headers["accept"] = "application/json; charset=utf-8"; return headers; }, }); } return reply.from(request.url.replace("/selenium/wd", ""), { body: request.body, rewriteRequestHeaders(request, headers) { headers["content-type"] = "application/json; charset=utf-8"; headers["accept"] = "application/json; charset=utf-8"; return headers; }, rewriteHeaders(headers, request) { headers["content-type"] = "application/json; charset=utf-8"; headers["accept"] = "application/json; charset=utf-8"; return headers; }, }); }, ); } export default routes; ================================================ FILE: api/src/modules/selenium/selenium.schema.ts ================================================ import { z } from "zod"; const LaunchRequest = z.object({ options: z.object({ args: z.array(z.string()).optional(), chromiumSandbox: z.boolean().optional(), devtools: z.boolean().optional(), downloadsPath: z.string().optional(), headless: z.boolean().optional(), ignoreDefaultArgs: z.union([z.boolean(), z.array(z.string())]).optional(), proxyUrl: z.string().optional(), timeout: z.number().optional(), tracesDir: z.string().optional(), }), req: z.any().optional(), stealth: z.boolean().optional(), cookies: z.array(z.any()).optional(), userAgent: z.string().optional(), extensions: z.array(z.string()).optional(), logSinkUrl: z.string().optional().describe("Deprecated"), customHeaders: z.record(z.string()).optional(), timezone: z.string().optional(), dimensions: z .object({ width: z.number(), height: z.number(), }) .nullable() .optional(), }); const LaunchResponse = z.object({ success: z.boolean(), }); export type LaunchRequest = z.infer; export const seleniumSchemas = { LaunchRequest, LaunchResponse, }; export default seleniumSchemas; ================================================ FILE: api/src/modules/sessions/sessions.controller.ts ================================================ import { CDPService } from "../../services/cdp/cdp.service.js"; import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { getErrors } from "../../utils/errors.js"; import { CreateSessionRequest, SessionDetails, SessionStreamRequest } from "./sessions.schema.js"; import { CookieData } from "../../services/context/types.js"; import { getUrl, getBaseUrl } from "../../utils/url.js"; export const handleLaunchBrowserSession = async ( server: FastifyInstance, request: CreateSessionRequest, reply: FastifyReply, ) => { try { const { sessionId, proxyUrl, userDataDir, persist, userAgent, sessionContext, extensions, logSinkUrl, timezone, dimensions, isSelenium, blockAds, optimizeBandwidth, extra, credentials, skipFingerprintInjection, userPreferences, deviceConfig, headless, } = request.body; return await server.sessionService.startSession({ sessionId, proxyUrl, userDataDir, persist, userAgent, sessionContext: sessionContext as { cookies?: CookieData[] | undefined; localStorage?: Record> | undefined; }, extensions, logSinkUrl, timezone, dimensions, isSelenium, blockAds, optimizeBandwidth, extra, credentials, skipFingerprintInjection, userPreferences, deviceConfig, headless, }); } catch (e: unknown) { server.log.error({ err: e }, "Failed lauching browser session"); const error = getErrors(e); return reply.code(500).send({ success: false, message: error }); } }; export const handleExitBrowserSession = async ( server: FastifyInstance, request: FastifyRequest, reply: FastifyReply, ) => { try { const sessionDetails = await server.sessionService.endSession(); reply.send({ success: true, ...sessionDetails }); } catch (e: any) { const error = getErrors(e); return reply.code(500).send({ success: false, message: error }); } }; export const handleGetBrowserContext = async ( browserService: CDPService, request: FastifyRequest, reply: FastifyReply, ) => { const context = await browserService.getBrowserState(); return reply.send(context); }; export const handleGetSessionDetails = async ( server: FastifyInstance, request: FastifyRequest<{ Params: { sessionId: string } }>, reply: FastifyReply, ) => { const sessionId = request.params.sessionId; if (sessionId !== server.sessionService.activeSession.id) { return reply.send({ id: sessionId, createdAt: new Date().toISOString(), status: "released", duration: 0, eventCount: 0, timeout: 0, creditsUsed: 0, websocketUrl: getBaseUrl("ws"), debugUrl: getUrl("v1/sessions/debug"), debuggerUrl: getUrl("v1/devtools/inspector.html"), sessionViewerUrl: getBaseUrl(), userAgent: "", isSelenium: false, proxy: "", proxyTxBytes: 0, proxyRxBytes: 0, solveCaptcha: false, } as SessionDetails); } const session = server.sessionService.activeSession; const duration = new Date().getTime() - new Date(session.createdAt).getTime(); console.log("duration", duration); return reply.send({ ...session, duration, }); }; export const handleGetSessions = async ( server: FastifyInstance, request: FastifyRequest, reply: FastifyReply, ) => { const currentSession = { ...server.sessionService.activeSession, duration: new Date().getTime() - new Date(server.sessionService.activeSession.createdAt).getTime(), }; const pastSessions = server.sessionService.pastSessions; return reply.send({ sessions: [currentSession, ...pastSessions] }); }; export const handleGetSessionStream = async ( server: FastifyInstance, request: SessionStreamRequest, reply: FastifyReply, ) => { const { showControls, theme, interactive, pageId, pageIndex } = request.query; const singlePageMode = !!(pageId || pageIndex); // Construct WebSocket URL with page parameters if present let wsUrl = getUrl("v1/sessions/cast", "ws"); if (pageId) { wsUrl += `?pageId=${encodeURIComponent(pageId)}`; } else if (pageIndex) { wsUrl += `?pageIndex=${encodeURIComponent(pageIndex)}`; } return reply.view("live-session-streamer.ejs", { wsUrl, showControls, theme, interactive, dimensions: server.sessionService.activeSession.dimensions, singlePageMode, }); }; export const handleGetSessionLiveDetails = async ( server: FastifyInstance, request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply, ) => { try { const pages = await server.cdpService.getAllPages(); const pagesInfo = await Promise.all( pages.map(async (page) => { try { const pageId = page.target()._targetId; const title = await page.title(); let favicon: string | null = null; try { favicon = await page.evaluate(() => { const iconLink = document.querySelector( 'link[rel="icon"], link[rel="shortcut icon"]', ); if (iconLink) { const href = iconLink.getAttribute("href"); if (href?.startsWith("http")) return href; if (href?.startsWith("//")) return window.location.protocol + href; if (href?.startsWith("/")) return window.location.origin + href; return window.location.origin + "/" + href; } return null; }); } catch (error) {} return { id: pageId, url: page.url(), title, favicon, }; } catch (error) { console.error("Error collecting page info:", error); return null; } }), ); const validPagesInfo = pagesInfo.filter((page) => page !== null); const browserVersion = await server.cdpService.getBrowserState(); const browserState = { status: server.sessionService.activeSession.status, userAgent: server.sessionService.activeSession.userAgent, browserVersion, initialDimensions: server.sessionService.activeSession.dimensions || { width: 1920, height: 1080, }, pageCount: validPagesInfo.length, }; return reply.send({ pages: validPagesInfo, browserState, websocketUrl: server.sessionService.activeSession.websocketUrl, sessionViewerUrl: server.sessionService.activeSession.sessionViewerUrl, sessionViewerFullscreenUrl: `${server.sessionService.activeSession.sessionViewerUrl}?showControls=false`, }); } catch (error) { console.error("Error getting session state:", error); return reply.code(500).send({ message: "Failed to get session state", error: getErrors(error), }); } }; ================================================ FILE: api/src/modules/sessions/sessions.routes.ts ================================================ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { handleLaunchBrowserSession, handleGetBrowserContext, handleExitBrowserSession, handleGetSessionDetails, handleGetSessions, handleGetSessionStream, handleGetSessionLiveDetails, } from "./sessions.controller.js"; import { handleScrape, handleScreenshot, handlePDF } from "../actions/actions.controller.js"; import { $ref } from "../../plugins/schemas.js"; import { CreateSessionRequest, RecordedEvents, SessionStreamRequest, SessionsScrapeRequest, SessionsScreenshotRequest, SessionsPDFRequest, } from "./sessions.schema.js"; import { BrowserEventType, EmitEvent } from "../../types/enums.js"; async function routes(server: FastifyInstance) { server.get( "/health", { schema: { operationId: "health", description: "Check if the server and browser are running", tags: ["Health"], summary: "Check if the server and browser are running", }, }, async (request: FastifyRequest, reply: FastifyReply) => { if (!server.cdpService.isRunning()) { return reply.status(503).send({ status: "service_unavailable" }); } return reply.send({ status: "ok" }); }, ); server.post( "/sessions", { schema: { operationId: "launch_browser_session", description: "Launch a browser session", tags: ["Sessions"], summary: "Launch a browser session", body: $ref("CreateSession"), response: { 200: $ref("SessionDetails"), }, }, }, async (request: CreateSessionRequest, reply: FastifyReply) => handleLaunchBrowserSession(server, request, reply), ); server.get( "/sessions", { schema: { operationId: "get_sessions", description: "Get all sessions", tags: ["Sessions"], summary: "Get all sessions", response: { 200: $ref("MultipleSessions"), }, }, }, async (request: FastifyRequest, reply: FastifyReply) => handleGetSessions(server, request, reply), ); server.get( "/sessions/:sessionId", { schema: { operationId: "get_session_details", description: "Get session details", tags: ["Sessions"], summary: "Get session details", response: { 200: $ref("SessionDetails"), }, }, }, async (request: FastifyRequest<{ Params: { sessionId: string } }>, reply: FastifyReply) => handleGetSessionDetails(server, request, reply), ); server.get( "/sessions/:sessionId/context", { schema: { operationId: "get_browser_context", description: "Get a browser context", tags: ["Sessions"], summary: "Get a browser context", response: { 200: $ref("SessionContextSchema"), }, }, }, async (request: FastifyRequest, reply: FastifyReply) => handleGetBrowserContext(server.cdpService, request, reply), ); server.post( "/sessions/:sessionId/release", { schema: { operationId: "release_browser_session", description: "Release a browser session", tags: ["Sessions"], summary: "Release a browser session", response: { 200: $ref("ReleaseSession"), }, }, }, async (request: FastifyRequest, reply: FastifyReply) => handleExitBrowserSession(server, request, reply), ); server.post( "/sessions/release", { schema: { operationId: "release_browser_sessions", description: "Release browser sessions", tags: ["Sessions"], summary: "Release browser sessions", response: { 200: $ref("ReleaseSession"), }, }, }, async (request: FastifyRequest, reply: FastifyReply) => handleExitBrowserSession(server, request, reply), ); server.get( "/sessions/debug", { onRequest: [], schema: { operationId: "get_session_debugger_stream", description: "Returns an HTML page with a live debugger view of the session", tags: ["Sessions"], summary: "Get session debugger view", querystring: $ref("SessionStreamQuery"), response: { 200: $ref("SessionStreamResponse"), }, }, }, async (request: SessionStreamRequest, reply: FastifyReply) => handleGetSessionStream(server, request, reply), ); server.post( "/events", { schema: { operationId: "receive_events", description: "Receive recorded events from the browser", tags: ["Sessions"], summary: "Receive recorded events from the browser", body: $ref("RecordedEvents"), }, }, async (request: FastifyRequest<{ Body: RecordedEvents }>, reply: FastifyReply) => { server.cdpService.getInstrumentationLogger().record({ type: BrowserEventType.Recording, timestamp: new Date().toISOString(), data: request.body, }); return reply.send({ status: "ok" }); }, ); server.get( "/sessions/:id/live-details", { onRequest: [], schema: { operationId: "get_session_live_details", description: "Returns the live state of the session, including pages, tabs, and browser state", tags: ["Sessions"], summary: "Get session live details", response: { 200: $ref("SessionLiveDetailsResponse"), }, }, }, async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => handleGetSessionLiveDetails(server, request, reply), ); server.post( "/sessions/scrape", { schema: { operationId: "scrape_session", description: "Scrape Current Session", tags: ["Sessions"], summary: "Scrape Current Session", body: $ref("ScrapeRequest"), response: { 200: $ref("ScrapeResponse"), }, }, }, async (request: SessionsScrapeRequest, reply: FastifyReply) => handleScrape(server.sessionService, server.cdpService, request, reply), ); server.post( "/sessions/screenshot", { schema: { operationId: "screenshot_session", description: "Take Screenshot of Current Session", tags: ["Sessions"], summary: "Take Screenshot of Current Session", body: $ref("ScreenshotRequest"), response: { 200: $ref("ScreenshotResponse"), }, }, }, async (request: SessionsScreenshotRequest, reply: FastifyReply) => handleScreenshot(server.sessionService, server.cdpService, request, reply), ); server.post( "/sessions/pdf", { schema: { operationId: "pdf_session", description: "Generate PDF of Current Session", tags: ["Sessions"], summary: "Generate PDF of Current Session", body: $ref("PDFRequest"), response: { 200: $ref("PDFResponse"), }, }, }, async (request: SessionsPDFRequest, reply: FastifyReply) => handlePDF(server.sessionService, server.cdpService, request, reply), ); } export default routes; ================================================ FILE: api/src/modules/sessions/sessions.schema.ts ================================================ import { FastifyRequest } from "fastify"; import { z } from "zod"; import { ScrapeRequestBody, ScreenshotRequestBody, PDFRequestBody, } from "../actions/actions.schema.js"; import { SessionContextSchema } from "../../services/context/types.js"; export type CredentialsOptions = z.infer; export const SessionCredentials = z .object({ autoSubmit: z.union([z.boolean(), z.never()]), blurFields: z.union([z.boolean(), z.never()]), exactOrigin: z.union([z.boolean(), z.never()]), }) .partial() .optional() .describe("Configuration for session credentials"); const CreateSession = z.object({ sessionId: z.string().uuid().optional().describe("Unique identifier for the session"), proxyUrl: z.string().optional().describe("Proxy URL to use for the session"), userAgent: z.string().optional().describe("User agent string to use for the session"), sessionContext: SessionContextSchema.optional().describe( "Session context data to be used in the created session", ), isSelenium: z.boolean().optional().describe("Indicates if Selenium is used in the session"), blockAds: z .boolean() .optional() .describe("Flag to indicate if ads should be blocked in the session"), optimizeBandwidth: z .union([ z.boolean(), z .object({ blockImages: z.boolean().optional(), blockMedia: z.boolean().optional(), blockStylesheets: z.boolean().optional(), blockHosts: z.array(z.string()).optional(), blockUrlPatterns: z.array(z.string()).optional(), }) .strict(), ]) .optional() .describe( "Enable bandwidth optimizations. Passing true enables all flags (except hosts/patterns). Object allows granular control.", ), skipFingerprintInjection: z .boolean() .optional() .describe("Flag to indicate if fingerprint injection should be skipped for this session."), deviceConfig: z .object({ device: z.enum(["desktop", "mobile"]).default("desktop"), }) .optional() .describe( "Device configuration for the session. Specify 'mobile' for mobile device fingerprints and configurations.", ), // Specific to hosted steel logSinkUrl: z.string().optional().describe("Deprecated: Log sink URL to use for the session"), extensions: z.array(z.string()).optional().describe("Extensions to use for the session"), persist: z.boolean().optional().describe("Flag to indicate if session should be persisted"), userDataDir: z.string().optional().describe("User data directory path to use for the session"), timezone: z.string().optional().describe("Timezone to use for the session"), dimensions: z .object({ width: z.number(), height: z.number(), }) .optional() .describe("Dimensions to use for the session"), userPreferences: z .record(z.string(), z.any()) .optional() .describe( "Chrome user preferences to customize browser behavior (e.g., font size, popup blocking, notification settings)", ), extra: z .record(z.string(), z.any()) .optional() .describe("Extra metadata to help initialize the session"), credentials: SessionCredentials, headless: z.boolean().optional().describe("Headless mode for the session"), }); const SessionDetails = z.object({ id: z.string().uuid().describe("Unique identifier for the session"), createdAt: z.string().datetime().describe("Timestamp when the session started"), status: z.enum(["idle", "live", "released", "failed"]).describe("Status of the session"), duration: z.number().int().describe("Duration of the session in milliseconds"), eventCount: z.number().int().describe("Number of events processed in the session"), dimensions: z .object({ width: z.number(), height: z.number(), }) .optional() .describe("Dimensions used for the session"), timeout: z.number().int().describe("Session timeout duration in milliseconds"), creditsUsed: z.number().int().describe("Amount of credits consumed by the session"), websocketUrl: z.string().describe("URL for the session's WebSocket connection"), debugUrl: z.string().describe("URL for a viewing the live browser instance for the session"), debuggerUrl: z.string().describe("URL for debugging the session"), sessionViewerUrl: z.string().describe("URL to view session details"), userAgent: z.string().optional().describe("User agent string used in the session"), proxy: z.string().optional().describe("Proxy server used for the session"), proxyTxBytes: z .number() .int() .nonnegative() .describe("Amount of data transmitted through the proxy"), proxyRxBytes: z .number() .int() .nonnegative() .describe("Amount of data received through the proxy"), solveCaptcha: z.boolean().optional().describe("Indicates if captcha solving is enabled"), isSelenium: z.boolean().optional().describe("Indicates if Selenium is used in the session"), }); const ReleaseSession = SessionDetails.merge( z.object({ success: z.boolean().describe("Indicates if the session was successfully released") }), ); const RecordedEvents = z.object({ events: z.array(z.any()).describe("Events to emit"), }); const SessionStreamQuery = z.object({ showControls: z .boolean() .optional() .default(true) .describe("Show controls in the browser iframe"), theme: z .enum(["dark", "light"]) .optional() .default("dark") .describe("Theme of the browser iframe"), interactive: z.boolean().optional().default(true).describe("Make the browser iframe interactive"), pageId: z.string().optional().describe("Page ID to connect to"), pageIndex: z.string().optional().describe("Page index (or tab index) to connect to"), }); const SessionLiveDetailsResponse = z.object({ sessionViewerUrl: z.string(), sessionViewerFullscreenUrl: z.string(), websocketUrl: z.string(), pages: z.array( z.object({ id: z.string(), url: z.string(), title: z.string(), favicon: z.string().nullable(), }), ), browserState: z.object({ status: z.enum(["idle", "live", "released", "failed"]), userAgent: z.string(), browserVersion: z.string(), initialDimensions: z.object({ width: z.number(), height: z.number(), }), pageCount: z.number(), }), }); const SessionStreamResponse = z.string().describe("HTML content for the session streamer view"); const MultipleSessions = z.object({ sessions: z.array(SessionDetails), }); export type SessionsScrapeRequestBody = Omit; export type SessionsScrapeRequest = FastifyRequest<{ Body: SessionsScrapeRequestBody }>; export type SessionsScreenshotRequestBody = Omit; export type SessionsScreenshotRequest = FastifyRequest<{ Body: SessionsScreenshotRequestBody }>; export type SessionsPDFRequestBody = Omit; export type SessionsPDFRequest = FastifyRequest<{ Body: SessionsPDFRequestBody }>; export type RecordedEvents = z.infer; export type CreateSessionBody = z.infer; export type CreateSessionRequest = FastifyRequest<{ Body: CreateSessionBody }>; export type SessionDetails = z.infer; export type MultipleSessions = z.infer; export type SessionStreamQuery = z.infer; export type SessionStreamRequest = FastifyRequest<{ Querystring: SessionStreamQuery }>; export const browserSchemas = { CreateSession, SessionDetails, MultipleSessions, SessionContextSchema, RecordedEvents, ReleaseSession, SessionStreamQuery, SessionStreamResponse, SessionLiveDetailsResponse, }; export default browserSchemas; ================================================ FILE: api/src/plugins/browser-session.ts ================================================ import { FastifyPluginAsync } from "fastify"; import fp from "fastify-plugin"; import { SessionService } from "../services/session.service.js"; const browserSessionPlugin: FastifyPluginAsync = async (fastify, _options) => { const sessionService = new SessionService({ cdpService: fastify.cdpService, seleniumService: fastify.seleniumService, fileService: fastify.fileService, logger: fastify.log, }); fastify.decorate("sessionService", sessionService); }; export default fp(browserSessionPlugin, "5.x"); ================================================ FILE: api/src/plugins/browser-socket/browser-socket.ts ================================================ import { type FastifyInstance, type FastifyPluginAsync } from "fastify"; import fp from "fastify-plugin"; import { WebSocketServer } from "ws"; import { WebSocketRegistryService } from "../../services/websocket-registry.service.js"; import { WebSocketHandler, WebSocketHandlerContext } from "../../types/websocket.js"; import { defaultHandlers } from "./handlers/index.js"; export interface BrowserSocketOptions { customHandlers?: WebSocketHandler[]; } // WebSocket server instance const wss = new WebSocketServer({ noServer: true }); const browserWebSocket: FastifyPluginAsync = async ( fastify: FastifyInstance, options: BrowserSocketOptions, ) => { if (!fastify.cdpService.isRunning()) { fastify.log.info("Launching browser..."); await fastify.cdpService.launch(); fastify.log.info("Browser launched successfully"); } const registry = new WebSocketRegistryService(); defaultHandlers.forEach((handler) => { registry.registerHandler(handler); }); if (options.customHandlers) { options.customHandlers.forEach((handler) => { registry.registerHandler(handler); }); } fastify.decorate("webSocketRegistry", registry); fastify.server.on("upgrade", async (request, socket, head) => { fastify.log.info("Upgrading browser socket..."); const url = request.url ?? ""; const params = Object.fromEntries( new URL(url || "", `http://${request.headers.host}`).searchParams.entries(), ); const context: WebSocketHandlerContext = { fastify, wss, params, }; const handler = registry.matchHandler(url); if (handler) { try { await handler.handler(request, socket, head, context); } catch (err) { fastify.log.error({ err }, `WebSocket handler error for ${url}`); socket.destroy(); } } else { fastify.log.info("Connecting to CDP..."); try { await fastify.cdpService.proxyWebSocket(request, socket, head); } catch (err) { fastify.log.error({ err }, "CDP WebSocket error"); socket.destroy(); } } }); }; export default fp(browserWebSocket, { name: "browser-websocket" }); ================================================ FILE: api/src/plugins/browser-socket/casting.handler.ts ================================================ import { IncomingMessage } from "http"; import puppeteer, { Browser, CDPSession, Page } from "puppeteer-core"; import { Duplex } from "stream"; import WebSocket, { Server } from "ws"; import { env } from "../../env.js"; import { SessionService } from "../../services/session.service.js"; import { CloseTabEvent, GetSelectedTextEvent, KeyEvent, MouseEvent, NavigationEvent, PageInfo, } from "../../types/casting.js"; import { getPageFavicon, getPageTitle, navigatePage } from "../../utils/casting.js"; export async function handleCastSession( request: IncomingMessage, socket: Duplex, head: Buffer, wss: Server, sessionService: SessionService, params: Record | undefined, ): Promise { const id = request.url?.split("/sessions/")[1].split("/cast")[0]; if (!id) { console.error("Cast Session ID not found"); socket.destroy(); return; } const session = await sessionService.activeSession; if (!session) { console.error(`Cast Session ${id} not found`); socket.destroy(); return; } const queryParams = new URLSearchParams(request.url?.split("?")[1] || ""); const requestedPageId = params?.pageId || queryParams.get("pageId") || null; const requestedPageIndex = params?.pageIndex || queryParams.get("pageIndex") || null; const tabDiscoveryMode = queryParams.get("tabInfo") === "true" || (!requestedPageId && !requestedPageIndex); const { height, width } = (session.dimensions as { width: number; height: number }) ?? { width: 1920, height: 1080, }; wss.handleUpgrade(request, socket, head, async (ws) => { let browser: Browser | null = null; let targetPage: Page | null = null; let targetClient: CDPSession | null = null; let targetPageId: string | null = null; const activePages = new Map(); let heartbeatInterval: NodeJS.Timeout | null = null; const handleSessionCleanup = () => { if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } if (targetPage) { targetPage.removeAllListeners("framenavigated"); } // Clean up screencast if (targetClient) { try { targetClient.send("Page.stopScreencast").catch((err) => { // Ignore errors about closed targets if (!err.message?.includes("Target closed")) { console.error("Error stopping screencast:", err); } }); targetClient.detach().catch((err) => { // Ignore errors about closed targets if (!err.message?.includes("Target closed")) { console.error("Error detaching client:", err); } }); targetClient = null; } catch (err) { console.error("Error during screencast cleanup:", err); } } // Disconnect browser if (browser) { try { browser.disconnect().catch((err) => { console.error("Error disconnecting browser:", err); }); browser = null; } catch (err) { console.error("Error during browser disconnect:", err); } } // Force garbage collection if available (Node.js with --expose-gc flag) if (global.gc) { try { global.gc(); } catch (err) { console.error("Error during garbage collection:", err); } } }; const sendTabList = async () => { try { if (ws.readyState !== WebSocket.OPEN || !tabDiscoveryMode) return; const tabList: PageInfo[] = []; for (const [pageId, page] of activePages.entries()) { tabList.push({ id: pageId, url: page.url(), title: await getPageTitle(page), favicon: await getPageFavicon(page), }); } ws.send( JSON.stringify({ type: "tabList", tabs: tabList, firstTabId: tabList.length > 0 ? tabList[0].id : null, }), ); } catch (error) { console.error("Error sending tab list:", error); } }; const findTargetPage = async ( pages: Page[], ): Promise<{ page: Page; pageId: string } | null> => { if (tabDiscoveryMode) return null; // No target page in tab discovery mode if (requestedPageId) { for (const page of pages) { try { //@ts-expect-error const pageId = page.target()._targetId; if (pageId === requestedPageId) { return { page, pageId }; } } catch (err) { console.error("Error accessing page target ID:", err); } } } else if (requestedPageIndex) { const index = parseInt(requestedPageIndex, 10); if (index >= 0 && index < pages.length) { const page = pages[index]; //@ts-expect-error const pageId = page.target()._targetId; return { page, pageId }; } } return null; }; try { browser = await puppeteer.connect({ browserWSEndpoint: `ws://${env.HOST}:${env.PORT}`, }); if (!browser) { console.error("Failed to connect to browser"); socket.destroy(); return; } const pages = await browser.pages(); if (tabDiscoveryMode) { for (const page of pages) { //@ts-expect-error const pageId = page.target()._targetId; activePages.set(pageId, page); } // Initial tab list await sendTabList(); // Setup page creation/deletion tracking browser.on("targetcreated", async (target) => { if (target.type() === "page") { try { const page = await target.asPage(); //@ts-expect-error const pageId = target._targetId; activePages.set(pageId, page); await sendTabList(); } catch (err) { console.error("Error handling new target:", err); } } }); browser.on("targetdestroyed", async (target) => { if (target.type() === "page") { try { //@ts-expect-error const pageId = target._targetId; if (activePages.has(pageId)) { activePages.delete(pageId); if (ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ type: "tabClosed", pageId, }), ); await sendTabList(); } } } catch (err) { console.error("Error handling destroyed target:", err); } } }); // Setup heartbeat to detect dead connections heartbeatInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { try { ws.ping(); } catch (err) { console.error("Error sending ping:", err); handleSessionCleanup(); } } else { handleSessionCleanup(); } }, 30000); ws.on("close", () => { handleSessionCleanup(); }); ws.on("error", (err) => { console.error("Tab discovery WebSocket error:", err); handleSessionCleanup(); }); return; } else { const targetResult = await findTargetPage(pages); if (!targetResult) { console.error( `Target page not found for ${ requestedPageId ? `pageId=${requestedPageId}` : `pageIndex=${requestedPageIndex}` }`, ); socket.destroy(); return; } targetPage = targetResult.page; targetPageId = targetResult.pageId; await targetPage.bringToFront(); // Setup screencast for the target page targetClient = await targetPage.target().createCDPSession(); ws.on("message", async (message) => { try { const data: | MouseEvent | KeyEvent | NavigationEvent | CloseTabEvent | GetSelectedTextEvent = JSON.parse(message.toString()); const { type } = data; if (!targetClient || !targetPage) { console.error("No target page or client available for input handling"); return; } switch (type) { case "mouseEvent": { const { event } = data as MouseEvent; await targetClient.send("Input.dispatchMouseEvent", { type: event.type, x: event.x, y: event.y, button: event.button, buttons: event.button === "none" ? 0 : 1, clickCount: event.clickCount || 1, modifiers: event.modifiers || 0, deltaX: event.deltaX, deltaY: event.deltaY, }); break; } case "keyEvent": { const { event } = data as KeyEvent; await targetClient.send("Input.dispatchKeyEvent", { type: event.type, text: event.text, unmodifiedText: event.text ? event.text.toLowerCase() : undefined, code: event.code, key: event.key, windowsVirtualKeyCode: event.keyCode, nativeVirtualKeyCode: event.keyCode, modifiers: event.modifiers || 0, autoRepeat: false, isKeypad: false, isSystemKey: false, }); break; } case "navigation": { const { event } = data as NavigationEvent; await navigatePage(event, targetPage); break; } case "closeTab": { const { pageId } = data as CloseTabEvent; await targetPage?.close(); if (activePages.has(pageId)) { activePages.delete(pageId); } break; } case "getSelectedText": { try { const selectedText = await targetPage.evaluate(() => { const selection = window.getSelection(); return selection ? selection.toString() : ""; }); // Send the selected text back to the client ws.send( JSON.stringify({ type: "selectedTextResponse", pageId: (data as GetSelectedTextEvent).pageId, text: selectedText, }), ); } catch (error) { console.error("Failed to get selected text:", error); ws.send( JSON.stringify({ type: "selectedTextResponse", pageId: (data as GetSelectedTextEvent).pageId, text: "", error: error instanceof Error ? error.message : "Unknown error", }), ); } break; } default: console.warn("Unknown event type:", type); } } catch (err) { console.error("Error handling WebSocket message:", err); } }); // Setup device metrics and start screencast await targetClient.send("Page.setDeviceMetricsOverride", { screenHeight: height, screenWidth: width, width, height, mobile: false, screenOrientation: { angle: 90, type: "landscapePrimary" }, deviceScaleFactor: 1, }); await targetClient.send("Page.startScreencast", { format: "jpeg", quality: 75, maxWidth: width, maxHeight: height, }); // Handle screencast frames targetClient.on("Page.screencastFrame", async ({ data, sessionId }) => { try { // Acknowledge the frame right away to free up memory await targetClient?.send("Page.screencastFrameAck", { sessionId }); if (ws.readyState === WebSocket.OPEN) { // Get page metadata const title = await getPageTitle(targetPage!); const favicon = await getPageFavicon(targetPage!); // Send frame data ws.send( JSON.stringify({ pageId: targetPageId, url: targetPage?.url(), title, favicon, data, }), ); } } catch (err) { console.error("Error in Page.screencastFrame handler:", err); } }); // Cleanup when target is destroyed browser.on("targetdestroyed", async (target) => { if (target.type() === "page") { try { //@ts-expect-error const pageId = target._targetId; if (pageId === targetPageId) { if (ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ type: "targetClosed", pageId: targetPageId, }), ); } // Cleanup and close connection handleSessionCleanup(); ws.close(); } } catch (err) { console.error("Error handling destroyed target:", err); } } }); // Setup heartbeat to detect dead connections heartbeatInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { try { ws.ping(); } catch (err) { console.error("Error sending ping:", err); handleSessionCleanup(); } } else { handleSessionCleanup(); } }, 30000); // Cleanup on WebSocket closure ws.on("close", () => { handleSessionCleanup(); }); // Handle errors ws.on("error", (err) => { console.error("Cast WebSocket error:", err); handleSessionCleanup(); }); } } catch (err) { console.error("Error in cast session:", err); handleSessionCleanup(); socket.destroy(); } }); } ================================================ FILE: api/src/plugins/browser-socket/handlers/cast.handler.ts ================================================ import { IncomingMessage } from "http"; import { Duplex } from "stream"; import { WebSocketHandler, WebSocketHandlerContext } from "../../../types/websocket.js"; import { handleCastSession } from "../casting.handler.js"; export const castHandler: WebSocketHandler = { path: "/v1/sessions/cast", handler: async ( request: IncomingMessage, socket: Duplex, head: Buffer, context: WebSocketHandlerContext, ) => { context.fastify.log.info("Connecting to cast..."); await handleCastSession( request, socket, head, context.wss, context.fastify.sessionService, context.params, ); }, }; ================================================ FILE: api/src/plugins/browser-socket/handlers/index.ts ================================================ export { logsHandler } from "./logs.handler.js"; export { castHandler } from "./cast.handler.js"; export { pageIdHandler } from "./pageId.handler.js"; export { recordingHandler } from "./recording.handler.js"; import { WebSocketHandler } from "../../../types/websocket.js"; import { logsHandler } from "./logs.handler.js"; import { castHandler } from "./cast.handler.js"; import { pageIdHandler } from "./pageId.handler.js"; import { recordingHandler } from "./recording.handler.js"; export const defaultHandlers: WebSocketHandler[] = [ logsHandler, castHandler, pageIdHandler, recordingHandler, ]; ================================================ FILE: api/src/plugins/browser-socket/handlers/logs.handler.ts ================================================ import { IncomingMessage } from "http"; import { Duplex } from "stream"; import { WebSocket } from "ws"; import { EmitEvent } from "../../../types/enums.js"; import { WebSocketHandler, WebSocketHandlerContext } from "../../../types/websocket.js"; function handleLogsWebSocket(context: WebSocketHandlerContext, ws: WebSocket) { const { fastify } = context; const messageHandler = (payload: { pageId: string }) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify([payload])); } }; fastify.cdpService.on(EmitEvent.Log, messageHandler); ws.on("error", (err) => { fastify.log.error({ err }, "Logs WebSocket error"); }); ws.on("close", () => { fastify.log.info("Logs WebSocket connection closed"); fastify.cdpService.removeListener(EmitEvent.Log, messageHandler); }); } export const logsHandler: WebSocketHandler = { path: "/v1/sessions/logs", handler: ( request: IncomingMessage, socket: Duplex, head: Buffer, context: WebSocketHandlerContext, ) => { context.fastify.log.info("Connecting to logs..."); context.wss.handleUpgrade(request, socket, head, (ws) => handleLogsWebSocket(context, ws)); }, }; ================================================ FILE: api/src/plugins/browser-socket/handlers/pageId.handler.ts ================================================ import { IncomingMessage } from "http"; import { Duplex } from "stream"; import { WebSocket } from "ws"; import { WebSocketHandler, WebSocketHandlerContext } from "../../../types/websocket.js"; function handlePageIdWebSocket(context: WebSocketHandlerContext, ws: WebSocket) { const { fastify } = context; const messageHandler = (payload: { pageId: string }) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(payload)); } }; fastify.cdpService.on("pageId", messageHandler); ws.on("error", (err) => { fastify.log.error({ err }, "PageId WebSocket error"); }); ws.on("close", () => { fastify.log.info("PageId WebSocket connection closed"); fastify.cdpService.removeListener("pageId", messageHandler); }); } export const pageIdHandler: WebSocketHandler = { path: "/v1/sessions/pageId", handler: ( request: IncomingMessage, socket: Duplex, head: Buffer, context: WebSocketHandlerContext, ) => { context.fastify.log.info("Connecting to pageId..."); context.wss.handleUpgrade(request, socket, head, (ws) => handlePageIdWebSocket(context, ws)); }, }; ================================================ FILE: api/src/plugins/browser-socket/handlers/recording.handler.ts ================================================ import { IncomingMessage } from "http"; import { Duplex } from "stream"; import { WebSocket } from "ws"; import { EmitEvent } from "../../../types/enums.js"; import { WebSocketHandler, WebSocketHandlerContext } from "../../../types/websocket.js"; function handleRecordingWebSocket(context: WebSocketHandlerContext, ws: WebSocket) { const { fastify } = context; const messageHandler = (payload: { events: Record[] }) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(payload.events)); } }; fastify.cdpService.on(EmitEvent.Recording, messageHandler); // TODO: handle inputs to browser from client ws.on("message", async (message) => {}); ws.on("close", () => { fastify.log.info("Recording WebSocket connection closed"); fastify.cdpService.removeListener(EmitEvent.Recording, messageHandler); }); ws.on("error", (err) => { fastify.log.error({ err }, "Recording WebSocket error"); }); } export const recordingHandler: WebSocketHandler = { path: "/v1/sessions/recording", handler: ( request: IncomingMessage, socket: Duplex, head: Buffer, context: WebSocketHandlerContext, ) => { context.fastify.log.info("Connecting to recording events..."); context.wss.handleUpgrade(request, socket, head, (ws) => handleRecordingWebSocket(context, ws)); }, }; ================================================ FILE: api/src/plugins/browser.ts ================================================ import { FastifyPluginAsync } from "fastify"; import { CDPService } from "../services/cdp/cdp.service.js"; import fp from "fastify-plugin"; import { BrowserLauncherOptions } from "../types/index.js"; import { DuckDBStorage, InMemoryStorage, LogStorage, } from "../services/cdp/instrumentation/storage/index.js"; import path from "path"; import os from "os"; import { env } from "../env.js"; declare module "fastify" { interface FastifyInstance { cdpService: CDPService; registerCDPLaunchHook: (hook: (config: BrowserLauncherOptions) => Promise | void) => void; registerCDPShutdownHook: ( hook: (config: BrowserLauncherOptions | null) => Promise | void, ) => void; } } const browserInstancePlugin: FastifyPluginAsync = async (fastify, _options) => { const loggingConfig = fastify.steelBrowserConfig?.logging || {}; const enableStorage = loggingConfig.enableStorage ?? env.LOG_STORAGE_ENABLED ?? false; const enableConsoleLogging = loggingConfig.enableConsoleLogging ?? true; let storage: LogStorage | null = null; if (enableStorage) { const storagePath = loggingConfig.storagePath || env.LOG_STORAGE_PATH || path.join(os.tmpdir(), "steel-browser-logs", "logs.duckdb"); storage = new DuckDBStorage({ dbPath: storagePath, maxThreads: 1, memoryLimit: "128MB", parquetCompression: "none", enableWriteBuffer: true, writeBufferSize: 200, writeBufferFlushInterval: 2000, }); await storage.initialize(); fastify.log.info(`Log storage initialized at ${storagePath}`); } else { // Use in-memory storage for development storage = new InMemoryStorage(1000); await storage.initialize(); fastify.log.info("Using in-memory log storage"); } const cdpService = new CDPService({}, fastify.log, storage, enableConsoleLogging); fastify.decorate("cdpService", cdpService); fastify.decorate( "registerCDPLaunchHook", (hook: (config: BrowserLauncherOptions) => Promise | void) => { cdpService.registerLaunchHook(hook); }, ); fastify.decorate( "registerCDPShutdownHook", (hook: (config: BrowserLauncherOptions | null) => Promise | void) => { cdpService.registerShutdownHook(hook); }, ); fastify.addHook("onListen", async function () { this.log.info("Launching default browser..."); await cdpService.launch(); }); }; export default fp(browserInstancePlugin, "5.x"); ================================================ FILE: api/src/plugins/custom-body-parser.ts ================================================ import fp from "fastify-plugin"; import { type FastifyInstance, type FastifyPluginAsync } from "fastify"; const customBodyParser: FastifyPluginAsync = async (fastify: FastifyInstance) => { fastify.addContentTypeParser( "application/json", { parseAs: "buffer" }, function (req, body, done) { try { switch (true) { case req.url.includes("/release"): // Skip parsing for release endpoints done(null, null); break; default: // Parse JSON for all other requests done(null, JSON.parse(body.toString())); } } catch (error) { done(error as Error, undefined); } }, ); }; export default fp(customBodyParser, { name: "custom-body-parser" }); ================================================ FILE: api/src/plugins/file-storage.ts ================================================ import { FastifyPluginAsync } from "fastify"; import fp from "fastify-plugin"; import { FileService } from "../services/file.service.js"; const fileStoragePlugin: FastifyPluginAsync = async (fastify, _options) => { fastify.log.info("Registering file service"); fastify.decorate("fileService", FileService.getInstance()); }; export default fp(fileStoragePlugin, "5.x"); ================================================ FILE: api/src/plugins/request-logger.ts ================================================ import { FastifyPluginAsync } from "fastify"; import fp from "fastify-plugin"; declare module "fastify" { interface FastifyReply { startTime: number; } } // https://github.com/fastify/fastify/blob/main/lib/logger.js#L67 function now() { const ts = process.hrtime(); return ts[0] * 1e3 + ts[1] / 1e6; } const logger: FastifyPluginAsync = async (fastify) => { fastify.addHook("onRequest", (req, reply, done) => { reply.startTime = now(); done(); }); fastify.addHook("onResponse", (req, reply, done) => { if ( req.method !== "OPTIONS" && req.raw.url !== "/status" && req.raw.url !== "/v1/events" && req.raw.url !== "/" ) { req.log.info( { ip: getClientIp(req), url: req.raw.url, method: req.method, statusCode: reply.raw.statusCode, durationMs: roundMS(now() - reply.startTime), }, "request completed", ); } done(); }); }; export default fp(logger); function roundMS(num: number): number { return Math.trunc(num * 100) / 100; } function getClientIp(req: any): string { if (req.headers["x-forwarded-for"]) { return req.headers["x-forwarded-for"].split(",")[0]; } if (req.headers["x-real-ip"]) { return req.headers["x-real-ip"]; } if (req.raw && req.raw.connection) { return req.raw.connection.remoteAddress || "unknown"; } if (req.raw && req.raw.socket) { return req.raw.socket.remoteAddress || "unknown"; } if (req.socket) { return req.socket.remoteAddress || "unknown"; } return "unknown"; } ================================================ FILE: api/src/plugins/scalar-theme.ts ================================================ export default ` /* Basic theme */ .light-mode { --scalar-color-1: #21201c; /* Sand 12 */ --scalar-color-2: #63635e; /* Sand 11 */ --scalar-color-3: #82827c; /* Sand 10 */ --scalar-color-accent: #1b180f; /* Yellow 9 */ --scalar-background-1: #fdfdfc; /* Sand 1 */ --scalar-background-2: #f9f9f8; /* Sand 2 */ --scalar-background-3: #f1f0ef; /* Sand 3 */ --scalar-background-accent: #ffe62a1f; --scalar-border-color: #f1f0ef; /* Sand 3 */ --scalar-code-language-color-supersede: var(--scalar-color-3); } .dark-mode { --scalar-color-1: #eeeeec; /* Sand 12 */ --scalar-color-2: #b5b3ad; /* Sand 11 */ --scalar-color-3: rgba(180, 179, 173, 0.6); /* Sand 10 with opacity */ --scalar-color-accent: #ffe62a; /* Yellow 9 */ --scalar-background-1: #111110; /* Sand 1 */ --scalar-background-2: #191918; /* Sand 2 */ --scalar-background-3: #222221; /* Sand 3 */ --scalar-background-accent: #ffe62a1f; --scalar-border-color: #222221; /* Sand Dark 3 */ --scalar-code-language-color-supersede: var(--scalar-color-3); } /* Document Sidebar */ .light-mode .t-doc__sidebar { --scalar-sidebar-background-1: var(--scalar-background-1); --scalar-sidebar-item-hover-color: currentColor; --scalar-sidebar-item-hover-background: var(--scalar-background-2); --scalar-sidebar-item-active-background: var(--scalar-background-accent); --scalar-sidebar-border-color: 0.5px solid var(--scalar-border-color); --scalar-sidebar-color-1: var(--scalar-color-1); --scalar-sidebar-color-2: var(--scalar-color-2); --scalar-sidebar-color-active: #1b180f; --scalar-sidebar-search-background: rgba(33, 32, 28, 0.05); --scalar-sidebar-search-border-color: 1px solid rgba(33, 32, 28, 0.05); --scalar-sidebar-search-color: var(--scalar-color-3); --scalar-background-2: rgba(33, 32, 28, 0.03); } .dark-mode .t-doc__sidebar { --scalar-sidebar-background-1: var(--scalar-background-1); --scalar-sidebar-item-hover-color: currentColor; --scalar-sidebar-item-hover-background: var(--scalar-background-2); --scalar-sidebar-item-active-background: rgba(238, 238, 236, 0.1); --scalar-sidebar-border-color: 0.5px solid var(--scalar-border-color); --scalar-sidebar-color-1: var(--scalar-color-1); --scalar-sidebar-color-2: var(--scalar-color-2); --scalar-sidebar-color-active: var(--scalar-color-accent); --scalar-sidebar-search-background: rgba(238, 238, 236, 0.1); --scalar-sidebar-search-border-color: 1px solid rgba(238, 238, 236, 0.05); --scalar-sidebar-search-color: var(--scalar-color-3); } /* Advanced */ .light-mode { --scalar-color-green: #069061; --scalar-color-red: #ef0006; --scalar-color-yellow: #edbe20; --scalar-color-blue: #0082d0; --scalar-color-orange: #fb892c; --scalar-color-purple: #5203d1; --scalar-button-1: rgba(33, 32, 28, 1); --scalar-button-1-hover: rgba(33, 32, 28, 0.8); --scalar-button-1-color: rgba(253, 253, 252, 0.9); } .dark-mode { --scalar-color-green: #00b648; --scalar-color-red: #dc1b19; --scalar-color-yellow: #ffc90d; --scalar-color-blue: #4eb3ec; --scalar-color-orange: #ff8d4d; --scalar-color-purple: #b191f9; --scalar-button-1: rgba(238, 238, 236, 1); --scalar-button-1-hover: rgba(238, 238, 236, 0.9); --scalar-button-1-color: #111110; } /* Custom Theme */ .dark-mode h2.t-editor__heading, .dark-mode .t-editor__page-title h1, .dark-mode h1.section-header, .dark-mode .markdown h1, .dark-mode .markdown h2, .dark-mode .markdown h3, .dark-mode .markdown h4, .dark-mode .markdown h5, .dark-mode .markdown h6 { -webkit-text-fill-color: transparent; background-image: linear-gradient( to right bottom, rgb(238, 238, 236) 30%, rgba(238, 238, 236, 0.38) ); -webkit-background-clip: text; background-clip: text; } .sidebar-search { backdrop-filter: blur(12px); } @keyframes headerbackground { from { background: transparent; backdrop-filter: none; } to { background: var(--scalar-header-background-1); backdrop-filter: blur(12px); } } .dark-mode .scalar-card { background: rgba(238, 238, 236, 0.05) !important; } .dark-mode .scalar-card * { --scalar-background-2: transparent !important; --scalar-background-1: transparent !important; } .light-mode .dark-mode.scalar-card *, .light-mode .dark-mode.scalar-card { --scalar-background-1: #191918 !important; --scalar-background-2: #191918 !important; --scalar-background-3: #222221 !important; } .light-mode .dark-mode.scalar-card { background: #222221 !important; } .badge { box-shadow: 0 0 0 1px var(--scalar-border-color); margin-right: 6px; } .table-row.required-parameter .table-row-item:nth-of-type(2):after { background: transparent; box-shadow: none; } /* Hero Section Flare */ .section-flare { width: 100vw; background: radial-gradient( ellipse 80% 50% at 50% -20%, rgba(255, 230, 42, 0.15), transparent ); height: 100vh; } /* Document layout */ .light-mode .t-doc .layout-content, .dark-mode .t-doc .layout-content { background: transparent; } .t-doc__header { background: rgba(253, 253, 252, 0.7); /* Sand 1 with opacity for light mode */ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); /* For Safari */ border-bottom: 0.5px solid var(--scalar-border-color); position: sticky; top: 0; z-index: 100; } .dark-mode .t-doc__header { background: rgba(17, 17, 16, 0.7); /* Sand 1 dark with opacity */ } /* Sidebar styles */ .t-doc__sidebar { background: rgba(253, 253, 252, 0.7); /* Sand 1 with opacity for light mode */ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 0.5px solid var(--scalar-border-color); z-index: 100; } .dark-mode .t-doc__sidebar { background: rgba(17, 17, 16, 0.7) !important; } `; ================================================ FILE: api/src/plugins/schemas.ts ================================================ import { FastifyPluginAsync } from "fastify"; import fp from "fastify-plugin"; import fastifySwagger from "@fastify/swagger"; import fastifyScalar from "@scalar/fastify-api-reference"; import { titleCase } from "../utils/text.js"; import actionSchemas from "../modules/actions/actions.schema.js"; import cdpSchemas from "../modules/cdp/cdp.schemas.js"; import logsSchemas from "../modules/logs/logs.schema.js"; import browserSchemas from "../modules/sessions/sessions.schema.js"; import seleniumSchemas from "../modules/selenium/selenium.schema.js"; import scalarTheme from "./scalar-theme.js"; import { buildJsonSchemas } from "../utils/schema.js"; import filesSchemas from "../modules/files/files.schema.js"; import { getBaseUrl } from "../utils/url.js"; const SCHEMAS = { ...actionSchemas, ...browserSchemas, ...logsSchemas, ...cdpSchemas, ...seleniumSchemas, ...filesSchemas, }; export const { schemas, $ref } = buildJsonSchemas(SCHEMAS); const schemaPlugin: FastifyPluginAsync = async (fastify) => { for (const schema of schemas) { fastify.addSchema(schema); } await fastify.register(fastifySwagger, { openapi: { info: { title: "Steel Browser Instance API", description: "Documentation for controlling a single instance of Steel Browser", version: "0.0.1", }, servers: [ { url: getBaseUrl(), description: "Local server", }, ], paths: {}, // paths must be included even if it's an empty object components: { securitySchemes: {}, }, }, refResolver: { buildLocalReference: (json, baseUri, fragment, i) => { return titleCase(json.$id as string) || `Fragment${i}`; }, }, }); await fastify.register(fastifyScalar as any, { // scalar still uses fastify v4 routePrefix: "/documentation", configuration: { customCss: scalarTheme, }, }); }; export default fp(schemaPlugin); ================================================ FILE: api/src/plugins/selenium.ts ================================================ import { FastifyPluginAsync } from "fastify"; import fp from "fastify-plugin"; import { SeleniumService } from "../services/selenium.service.js"; const seleniumPlugin: FastifyPluginAsync = async (fastify, options) => { const seleniumService = new SeleniumService(fastify.log); fastify.decorate("seleniumService", seleniumService); }; export default fp(seleniumPlugin, "5.x"); ================================================ FILE: api/src/plugins/ui-plugin.ts ================================================ import fastifyStatic from "@fastify/static"; import { FastifyPluginAsync, FastifyRequest, FastifyReply } from "fastify"; import fp from "fastify-plugin"; import path from "node:path"; import fs from "node:fs"; export interface UIPluginOptions { uiDistPath?: string; uiPrefix?: string; } const uiPlugin: FastifyPluginAsync = async (fastify, opts) => { const uiDistPath = opts.uiDistPath || path.join(process.cwd(), "ui/dist"); const uiPrefix = opts.uiPrefix || "/ui"; if (!fs.existsSync(uiDistPath)) { fastify.log.info("UI dist not found, skipping UI serving"); return; } fastify.log.info(`UI plugin activated: serving from ${uiDistPath} at ${uiPrefix}`); await fastify.register(fastifyStatic, { root: uiDistPath, prefix: uiPrefix, decorateReply: true, }); fastify.get("/", async (request: FastifyRequest, reply: FastifyReply) => { const userAgent = request.headers["user-agent"]; // If it's a browser, redirect to UI if (userAgent && userAgent.includes("Mozilla")) { return reply.redirect(uiPrefix); } return { message: "Steel Browser API", ui: uiPrefix }; }); fastify.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => { const url = request.url; if (url.startsWith(uiPrefix)) { return reply.sendFile("index.html"); } return reply.code(404).send({ error: "Not Found" }); }); fastify.log.info("UI plugin registered successfully"); }; export default fp(uiPlugin, { name: "ui-plugin", fastify: "5.x", }); ================================================ FILE: api/src/routes.ts ================================================ export { default as actionsRoutes } from "./modules/actions/actions.routes.js"; export { default as sessionsRoutes } from "./modules/sessions/sessions.routes.js"; export { default as seleniumRoutes } from "./modules/selenium/selenium.routes.js"; export { default as cdpRoutes } from "./modules/cdp/cdp.routes.js"; export { default as filesRoutes } from "./modules/files/files.routes.js"; export { default as logsRoutes } from "./modules/logs/logs.routes.js"; ================================================ FILE: api/src/scripts/fingerprint.js ================================================ const _0x28f974 = _0x5610; (function (_0x3ccf48, _0x290ea1) { const _0x17ecfd = _0x5610, _0xa8e50e = _0x3ccf48(); while (!![]) { try { const _0xb7d3fe = -parseInt(_0x17ecfd(0x155)) / 0x1 + (-parseInt(_0x17ecfd(0x198)) / 0x2) * (-parseInt(_0x17ecfd(0xa3)) / 0x3) + -parseInt(_0x17ecfd(0x10e)) / 0x4 + (-parseInt(_0x17ecfd(0x108)) / 0x5) * (-parseInt(_0x17ecfd(0x182)) / 0x6) + -parseInt(_0x17ecfd(0xe0)) / 0x7 + (-parseInt(_0x17ecfd(0xe1)) / 0x8) * (parseInt(_0x17ecfd(0x99)) / 0x9) + (-parseInt(_0x17ecfd(0xae)) / 0xa) * (-parseInt(_0x17ecfd(0x154)) / 0xb); if (_0xb7d3fe === _0x290ea1) break; else _0xa8e50e["push"](_0xa8e50e["shift"]()); } catch (_0x2e18c7) { _0xa8e50e["push"](_0xa8e50e["shift"]()); } } })(_0x338a, 0x89a89); const originalConsoleDebug = console[_0x28f974(0x113)], originalConsoleLog = console[_0x28f974(0x12f)]; (console[_0x28f974(0x113)] = function () {}), (console["log"] = function () { const _0x5571e0 = _0x28f974, _0x29b17d = new Error()[_0x5571e0(0x123)] || ""; if ( !( _0x29b17d[_0x5571e0(0x9d)]("chrome-ext" + _0x5571e0(0xbe)) || _0x29b17d["includes"](_0x5571e0(0x175) + "/") || _0x29b17d[_0x5571e0(0x9d)](_0x5571e0(0x105)) ) ) return originalConsoleLog[_0x5571e0(0x9b)](this, arguments); }), delete window["cdc_adoQpo" + _0x28f974(0xb0) + _0x28f974(0x148) + "ay"], delete window[_0x28f974(0xf4) + _0x28f974(0xb0) + _0x28f974(0x133) + _0x28f974(0x18b)], delete window[_0x28f974(0xf4) + _0x28f974(0xb0) + _0x28f974(0x9c) + _0x28f974(0x193)]; function _0x5610(_0xa98ee6, _0x384755) { const _0x338afa = _0x338a(); return ( (_0x5610 = function (_0x561077, _0x486847) { _0x561077 = _0x561077 - 0x99; let _0x5b5815 = _0x338afa[_0x561077]; return _0x5b5815; }), _0x5610(_0xa98ee6, _0x384755) ); } const originalHardwareConcurrency = navigator[_0x28f974(0x10f) + _0x28f974(0x150)], originalDeviceMemory = navigator[_0x28f974(0x164) + "ry"] || 0x8; delete navigator[_0x28f974(0x10f) + _0x28f974(0x150)], delete navigator["deviceMemo" + "ry"]; const originalGetOwnPropertyNames = Object["getOwnProp" + _0x28f974(0xf6)]; function _0x338a() { const _0x499256 = [ "ERSION)\x20re", "getOwnProp", "omium)\x27;\x20\x20", "Type\x20===\x20\x27", "ames(obj);", "getSupport", "meter\x20===\x20", "alse,\x20\x20\x20\x20c", "return\x20pro", ".\x20(NVIDIA)", "==\x20ctx.VER", "rn\x20\x27ANGLE\x20", "sole.log\x20=", "keys", "og\x20=\x20funct", "L\x20ES\x20GLSL\x20", "ENDERER_WE", "WEBGL_debu", "asOwnPrope", "\x20prop.toSt", ")\x20return\x20\x27", "r.hardware", "al-webgl", "onfigurabl", "lse,\x20\x20\x20\x20co", "ameter);\x20\x20", "igDeviceMe", "peof\x20Offsc", "er;\x20\x20\x20\x20\x20\x20c", "debug_rend", "5465187IyJpyM", "20392UGBQMn", "1.0\x20(OpenG", "ameter)\x20{\x20", "\x20prop\x20===\x20", "\x20\x20\x20if\x20(par", "BGL", "&\x20prop\x20!==", "ER)\x20return", "e\x20navigato", "LANGUAGE_V", "nfigurable", "creenCanva", "inalOffscr", "erties", "}\x20\x20\x20\x20retur", "e:\x20false,\x20", "ole.debug\x20", "turn\x20\x27WebG", "\x22\x20||\x20\x20\x20\x20\x20\x20", "cdc_adoQpo", "fined\x27)\x20{\x20", "ertyNames", "reenCanvas", "experiment", "}});if\x20(ty", "edExtensio", "=\x20function", "UNMASKED_R", "ion(contex", ":\x20false\x20\x20}", "n/javascri", "=\x20Object.p", "ctx.RENDER", "return\x20ori", "erer_info\x27", "ter\x20===\x20ct", "chrome://", "ludes(\x22CDP", "igator.dev", "307365eYbXvj", "s.prototyp", "\x22);", "extType\x20==", "Memory\x22:\x20{", "urrency\x22\x20&", "4191028ywmhdl", "hardwareCo", "VERSION", "VIDIA,\x20NVI", "\x20\x22Runtime\x22", "debug", "on(\x27WEBGL_", "mory\x20=\x20nav", "console.de", "Concurrenc", "pe.getCont", "ring().inc", "RENDERER", "\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20", "meter\x20=\x20fu", "DIA\x20GeForc", "\x20\x20\x20\x20value:", "merable:\x20f", "op)\x20{\x20\x20\x20if", "ctx\x20=\x20orig", "rce\x20GTX\x2010", "stack", "\x20{\x20\x20\x20\x20\x20\x20\x20\x20", "erable:\x20fa", "DOR_WEBGL)", "tx.getPara", "Google\x20Inc", "pertyNames", "\x20\x20if\x20(para", "ameter\x20===", ".UNMASKED_", "RENDERER_W", "gInfo)\x20{\x20\x20", "log", "tType,\x20att", "\x27;\x20\x20\x20\x20\x20\x20\x20\x20", "xtType,\x20at", "ZLmcfl_Pro", "if\x20(parame", "const\x20orig", "t.prototyp", "webgl2\x27))\x20", "revokeObje", "nProperty.", "filter", "=\x20\x27webgl\x27\x20", "push", "tch\x20(e)\x20{}", "Worker", "y;const\x20or", "lGetParame", "e.getConte", "(this,\x20par", "\x20const\x20ori", "his,\x20conte", "\x20!==\x20\x27unde", "ion()\x20{};\x20", "nfo\x20=\x20ctx.", "ZLmcfl_Arr", "\x20\x20if\x20(obj\x20", "\x20return\x20\x27G", "||\x20context", ")\x20{};\x20self", "opertyName", "eenGetCont", "(navigator", "ncurrency", "8,\x20\x20\x20\x20enum", "HardwareCo", ".console.l", "17666kTcZDo", "1035925IHssXc", "nction(par", "al-webgl\x27\x20", "ginalOffsc", "g_renderer", "ter\x20=\x20ctx.", "11)\x27;\x20\x20\x20\x20\x20", "==\x20\x22Devtoo", "meter.call", "bj)\x20{\x20\x20con", "ter\x20===\x20de", "lsProtocol", "ENDOR_WEBG", ",\x20{\x20\x20\x22hard", "\x20\x20\x20\x20\x20\x20\x20}\x20\x20", "deviceMemo", "webgl2", "\x20\x20\x20\x20if\x20(ct", "rency\x22:\x20{\x20", "oogle\x20Inc.", "call", "(NVIDIA,\x20N", "as.prototy", "operty\x20=\x20f", "\x20\x20\x20value:\x20", "x\x20&&\x20(cont", "\x20Direct3D1", "nst\x20debugI", "\x20\x20\x20\x20};\x20\x20\x20\x20", "defineProp", "\x20||\x20prop\x20=", "\x20\x22deviceMe", "devtools:/", "webgl", "SION)\x20retu", "getContext", "s\x20=\x20Object", "VENDOR", "bug\x20=\x20func", "ext;\x20\x20Offs", "\x20\x20return\x20p", "UNMASKED_V", "\x20(prop\x20===", "reenGetCon", "tor)\x20{\x20\x20\x20\x20", "42ZUMQMn", "arameter\x20=", "ps.filter(", "\x20debugInfo", "screenCanv", "iceMemory\x20", "ctx.VENDOR", "\x20\x20\x20\x20const\x20", "turn\x20origi", "mise", "getParamet", ".getOwnPro", "xt\x20=\x20funct", "getExtensi", "aluate\x22\x20||", "rops;};Obj", "NGUAGE_VER", "bol", "n\x20ctx;\x20\x20};", "\x22runtimeEv", "\x20origGetOw", "text\x20=\x20Off", "1301452EXqqhZ", "99mxnLNq", "se;\x20\x20\x20}\x20\x20\x20", "apply", "ZLmcfl_Sym", "includes", "\x20prop);\x20};", "=\x20navigato", "1\x20vs_5_0\x20p", "_info", "\x20\x20\x20\x20\x20\x20}\x20ca", "3AhjcWK", "ctURL", "===\x20naviga", "\x20\x20if\x20(debu", "wareConcur", "3D11)\x27;\x20\x20\x20", "SHADING_LA", "tOwnProper", "\x20function(", "hromium)\x27;", "70\x20Direct3", "14790hINtqh", "\x20\x20writable", "asnfa76pfc", "SION", "()\x20{};\x20con", "op\x20!==\x20\x22ha", "avigator.d", "applicatio", "rdwareConc", "\x20\x20\x20\x20\x20if\x20(p", "\x20(NVIDIA)\x27", "\x208,\x20\x20\x20\x20enu", "\x20self.cons", "nalGetPara", "prototype", "ginalHasOw", "ension://", "tion()\x20{};", "e.hasOwnPr", "nPropertyN", ]; _0x338a = function () { return _0x499256; }; return _0x338a(); } Object[_0x28f974(0xc3) + _0x28f974(0xf6)] = function (_0x172b79) { const _0x461a49 = _0x28f974, _0x25bce7 = originalGetOwnPropertyNames(_0x172b79); return _0x172b79 === navigator ? _0x25bce7[_0x461a49(0x13a)]( (_0x16eafa) => _0x461a49(0x10f) + _0x461a49(0x150) !== _0x16eafa && _0x461a49(0x164) + "ry" !== _0x16eafa, ) : _0x25bce7; }; const originalObjectKeys = Object["keys"]; (Object[_0x28f974(0xcf)] = function (_0x2c0550) { const _0x1bf546 = _0x28f974, _0x251c2c = originalObjectKeys(_0x2c0550); return _0x2c0550 === navigator ? _0x251c2c[_0x1bf546(0x13a)]( (_0x576892) => _0x1bf546(0x10f) + _0x1bf546(0x150) !== _0x576892 && "deviceMemo" + "ry" !== _0x576892, ) : _0x251c2c; }), Object[_0x28f974(0x172) + _0x28f974(0xee)](navigator, { hardwareConcurrency: { value: FIXED_HARDWARE_CONCURRENCY, enumerable: !0x1, configurable: !0x1, writable: !0x1 }, deviceMemory: { value: FIXED_DEVICE_MEMORY, enumerable: !0x1, configurable: !0x1, writable: !0x1 }, }); const mockWebGLParameters = { 0x1f00: [0x0, 0x0, 0x0, 0x0], 0x8894: [0x0, 0x0, 0x0, 0x0], 0x8ca6: 0x4000, 0x85b5: 0x1, 0x8cab: 0x10, 0x8b4a: 0x20, 0x8b4b: 0x10, 0x8a2a: 0x4000, 0x8824: 0x20, 0x8827: 0x800, 0x8b4c: 0x800, 0x8872: 0x8, 0x8b49: 0x0, 0x8b8d: 0x0, 0x8b8d: 0x0, 0x8b8b: 0x0, 0x8b88: 0x0, 0x8dfa: 0x20, 0x8dfb: 0x20, 0x8dfc: 0x10, 0x9120: 0x0, 0x9240: 0x0, 0x9241: 0x0, 0x9242: 0x0, 0x9243: 0x0, 0x9244: 0x0, 0x9245: 0x0, }, fixWebGLContext = () => { const _0x20f634 = _0x28f974, _0x855c5e = HTMLCanvasElement[_0x20f634(0xbc)]["getContext"]; if ( ((HTMLCanvasElement["prototype"][_0x20f634(0x178)] = function (_0x2f7e95, _0x2f8dc2) { const _0x5c8803 = _0x20f634, _0x5deb25 = _0x855c5e[_0x5c8803(0x169)](this, _0x2f7e95, _0x2f8dc2); if ( _0x5deb25 && (_0x5c8803(0x176) === _0x2f7e95 || _0x5c8803(0xf8) + _0x5c8803(0xd8) === _0x2f7e95 || _0x5c8803(0x165) === _0x2f7e95) ) { const _0x3883c3 = _0x5deb25[_0x5c8803(0x18c) + "er"], _0x2131ac = _0x5deb25["getExtensi" + "on"], _0x86a1d3 = _0x5deb25[_0x5c8803(0xc7) + _0x5c8803(0xfa) + "ns"]; (_0x5deb25[_0x5c8803(0x18c) + "er"] = function (_0x400a58) { const _0x50ab3f = _0x5c8803; try { if (_0x400a58 === _0x5deb25[_0x50ab3f(0x17a)]) return FIXED_VENDOR; if (_0x400a58 === _0x5deb25[_0x50ab3f(0x11a)]) return FIXED_RENDERER; if (_0x400a58 === _0x5deb25[_0x50ab3f(0x110)]) return FIXED_VERSION; if (_0x400a58 === _0x5deb25["SHADING_LA" + _0x50ab3f(0x192) + _0x50ab3f(0xb1)]) return FIXED_SHADING_LANGUAGE_VERSION; if (void 0x0 !== mockWebGLParameters[_0x400a58]) return mockWebGLParameters[_0x400a58]; const _0x32229c = _0x5deb25[_0x50ab3f(0x18f) + "on"]( _0x50ab3f(0xd3) + _0x50ab3f(0x159) + _0x50ab3f(0xa1), ); if (_0x32229c) { if (_0x400a58 === _0x32229c[_0x50ab3f(0x17e) + _0x50ab3f(0x161) + "L"]) return FIXED_VENDOR; if (_0x400a58 === _0x32229c["UNMASKED_R" + _0x50ab3f(0xd2) + _0x50ab3f(0xe6)]) return FIXED_RENDERER; } } catch (_0xbf2904) {} return _0x3883c3[_0x50ab3f(0x169)](this, _0x400a58); }), (_0x5deb25[_0x5c8803(0x18f) + "on"] = function (_0x5229be) { const _0x30f88a = _0x5c8803; if (_0x30f88a(0xd3) + _0x30f88a(0x159) + _0x30f88a(0xa1) === _0x5229be) { const _0x39aa64 = _0x2131ac[_0x30f88a(0x169)](this, _0x5229be); return ( _0x39aa64 && Object[_0x30f88a(0x172) + "erties"](_0x39aa64, { UNMASKED_VENDOR_WEBGL: { value: 0x9245, enumerable: !0x0 }, UNMASKED_RENDERER_WEBGL: { value: 0x9246, enumerable: !0x0 }, }), _0x39aa64 ); } return _0x2131ac[_0x30f88a(0x169)](this, _0x5229be); }), (_0x5deb25[_0x5c8803(0xc7) + "edExtensio" + "ns"] = function () { const _0x3c16f7 = _0x5c8803, _0x2d3b38 = _0x86a1d3["call"](this) || []; return ( _0x2d3b38[_0x3c16f7(0x9d)](_0x3c16f7(0xd3) + _0x3c16f7(0x159) + _0x3c16f7(0xa1)) || _0x2d3b38["push"](_0x3c16f7(0xd3) + _0x3c16f7(0x159) + _0x3c16f7(0xa1)), _0x2d3b38 ); }); } return _0x5deb25; }), "undefined" != typeof OffscreenCanvas) ) { const _0x4a1f75 = OffscreenCanvas["prototype"][_0x20f634(0x178)]; OffscreenCanvas[_0x20f634(0xbc)][_0x20f634(0x178)] = function (_0x1fc75c, _0x10362e) { const _0x17c4cd = _0x20f634, _0x4f8a14 = _0x4a1f75["call"](this, _0x1fc75c, _0x10362e); if ( _0x4f8a14 && (_0x17c4cd(0x176) === _0x1fc75c || _0x17c4cd(0xf8) + _0x17c4cd(0xd8) === _0x1fc75c || _0x17c4cd(0x165) === _0x1fc75c) ) { const _0x1fc28e = _0x4f8a14[_0x17c4cd(0x18c) + "er"], _0x3245bb = _0x4f8a14[_0x17c4cd(0x18f) + "on"], _0xc07c9b = _0x4f8a14["getSupport" + _0x17c4cd(0xfa) + "ns"]; (_0x4f8a14[_0x17c4cd(0x18c) + "er"] = function (_0x30eb46) { const _0x13cc6e = _0x17c4cd; try { if (_0x30eb46 === _0x4f8a14["VENDOR"]) return FIXED_VENDOR; if (_0x30eb46 === _0x4f8a14[_0x13cc6e(0x11a)]) return FIXED_RENDERER; if (_0x30eb46 === _0x4f8a14["VERSION"]) return FIXED_VERSION; if (_0x30eb46 === _0x4f8a14[_0x13cc6e(0xa9) + "NGUAGE_VER" + _0x13cc6e(0xb1)]) return FIXED_SHADING_LANGUAGE_VERSION; if (void 0x0 !== mockWebGLParameters[_0x30eb46]) return mockWebGLParameters[_0x30eb46]; const _0x363fa1 = _0x4f8a14[_0x13cc6e(0x18f) + "on"](_0x13cc6e(0xd3) + "g_renderer" + "_info"); if (_0x363fa1) { if (_0x30eb46 === _0x363fa1[_0x13cc6e(0x17e) + _0x13cc6e(0x161) + "L"]) return FIXED_VENDOR; if (_0x30eb46 === _0x363fa1[_0x13cc6e(0xfc) + _0x13cc6e(0xd2) + _0x13cc6e(0xe6)]) return FIXED_RENDERER; } } catch (_0x322b9c) {} return _0x1fc28e[_0x13cc6e(0x169)](this, _0x30eb46); }), (_0x4f8a14[_0x17c4cd(0x18f) + "on"] = function (_0x4cafa2) { const _0x155688 = _0x17c4cd; if ("WEBGL_debu" + "g_renderer" + _0x155688(0xa1) === _0x4cafa2) { const _0x5a6270 = _0x3245bb[_0x155688(0x169)](this, _0x4cafa2); return ( _0x5a6270 && Object["defineProp" + _0x155688(0xee)](_0x5a6270, { UNMASKED_VENDOR_WEBGL: { value: 0x9245, enumerable: !0x0 }, UNMASKED_RENDERER_WEBGL: { value: 0x9246, enumerable: !0x0 }, }), _0x5a6270 ); } return _0x3245bb[_0x155688(0x169)](this, _0x4cafa2); }), (_0x4f8a14[_0x17c4cd(0xc7) + "edExtensio" + "ns"] = function () { const _0x168aa7 = _0x17c4cd, _0xab77ad = _0xc07c9b[_0x168aa7(0x169)](this) || []; return ( _0xab77ad[_0x168aa7(0x9d)](_0x168aa7(0xd3) + _0x168aa7(0x159) + _0x168aa7(0xa1)) || _0xab77ad[_0x168aa7(0x13c)](_0x168aa7(0xd3) + _0x168aa7(0x159) + "_info"), _0xab77ad ); }); } return _0x4f8a14; }; } }, originalWorker = window[_0x28f974(0x13e)]; (window["Worker"] = function (_0x4ff437, _0x593dbe) { const _0x561dfa = _0x28f974, _0x15b35e = new Blob( [ _0x561dfa(0x116) + _0x561dfa(0x17b) + _0x561dfa(0xbf) + _0x561dfa(0xba) + _0x561dfa(0xf1) + _0x561dfa(0xfb) + _0x561dfa(0xb2) + _0x561dfa(0xce) + _0x561dfa(0xab) + _0x561dfa(0x14c) + _0x561dfa(0x153) + _0x561dfa(0xd0) + _0x561dfa(0x146) + _0x561dfa(0x135) + _0x561dfa(0x152) + "ncurrency\x20" + _0x561dfa(0x9f) + _0x561dfa(0xd7) + _0x561dfa(0x117) + _0x561dfa(0x13f) + _0x561dfa(0xdc) + _0x561dfa(0x115) + _0x561dfa(0x107) + _0x561dfa(0x187) + "||\x208;delet" + _0x561dfa(0xe9) + _0x561dfa(0xd7) + "Concurrenc" + "y;delete\x20n" + _0x561dfa(0xb4) + "eviceMemor" + "y;const\x20or" + "igGetOwnPr" + _0x561dfa(0x14d) + _0x561dfa(0x179) + _0x561dfa(0x18d) + _0x561dfa(0x129) + ";Object.ge" + _0x561dfa(0xaa) + "tyNames\x20=\x20" + "function(o" + _0x561dfa(0x15e) + "st\x20props\x20=" + _0x561dfa(0x196) + _0x561dfa(0xc1) + _0x561dfa(0xc6) + _0x561dfa(0x149) + _0x561dfa(0xa5) + _0x561dfa(0x181) + _0x561dfa(0xca) + _0x561dfa(0x184) + "prop\x20=>\x20pr" + _0x561dfa(0xb3) + _0x561dfa(0xb6) + _0x561dfa(0x10d) + _0x561dfa(0xe7) + _0x561dfa(0x174) + "mory\x22);\x20\x20}" + _0x561dfa(0x17d) + _0x561dfa(0x191) + "ect.define" + "Properties" + _0x561dfa(0x14f) + _0x561dfa(0x162) + _0x561dfa(0xa7) + _0x561dfa(0x167) + _0x561dfa(0x16d) + _0x561dfa(0x151) + _0x561dfa(0x125) + _0x561dfa(0xda) + _0x561dfa(0xeb) + ":\x20false,\x20\x20" + _0x561dfa(0xaf) + _0x561dfa(0xfe) + ",\x20\x20\x22device" + _0x561dfa(0x10c) + _0x561dfa(0x11e) + _0x561dfa(0xb9) + _0x561dfa(0x11f) + _0x561dfa(0xc9) + _0x561dfa(0xd9) + _0x561dfa(0xf0) + "\x20\x20\x20writabl" + "e:\x20false\x20\x20" + _0x561dfa(0xf9) + _0x561dfa(0xdd) + _0x561dfa(0xf7) + _0x561dfa(0x145) + _0x561dfa(0xf5) + _0x561dfa(0x143) + _0x561dfa(0x158) + _0x561dfa(0x180) + _0x561dfa(0x197) + _0x561dfa(0x186) + _0x561dfa(0x16b) + _0x561dfa(0x118) + _0x561dfa(0x17c) + _0x561dfa(0xec) + _0x561dfa(0x109) + _0x561dfa(0x141) + (_0x561dfa(0x18e) + _0x561dfa(0xfd) + _0x561dfa(0x130) + "ributes)\x20{" + _0x561dfa(0x189) + _0x561dfa(0x121) + _0x561dfa(0xed) + _0x561dfa(0x14e) + "ext.call(t" + _0x561dfa(0x144) + _0x561dfa(0x132) + "tributes);" + _0x561dfa(0x166) + _0x561dfa(0x16e) + _0x561dfa(0x10b) + _0x561dfa(0x13b) + _0x561dfa(0x14b) + "Type\x20===\x20\x27" + _0x561dfa(0xf8) + _0x561dfa(0x157) + "||\x20context" + _0x561dfa(0xc5) + _0x561dfa(0x137) + "{\x20\x20\x20\x20\x20\x20con" + "st\x20origina" + _0x561dfa(0x140) + _0x561dfa(0x15a) + _0x561dfa(0x18c) + _0x561dfa(0xde) + _0x561dfa(0x127) + _0x561dfa(0x11c) + _0x561dfa(0x156) + _0x561dfa(0xe3) + "\x20\x20\x20\x20\x20\x20\x20try" + _0x561dfa(0x124) + "\x20\x20if\x20(para" + _0x561dfa(0xc8) + _0x561dfa(0x188) + _0x561dfa(0xd6) + _0x561dfa(0x128) + _0x561dfa(0xcb) + _0x561dfa(0x131) + _0x561dfa(0x12a) + "meter\x20===\x20" + _0x561dfa(0x101) + _0x561dfa(0xe8) + "\x20\x27ANGLE\x20(N" + _0x561dfa(0x111) + _0x561dfa(0x11d) + "e\x20GTX\x201070" + _0x561dfa(0x16f) + _0x561dfa(0xa0) + "s_5_0,\x20D3D" + _0x561dfa(0x15b) + _0x561dfa(0xb7) + _0x561dfa(0x183) + _0x561dfa(0xcc) + _0x561dfa(0x177) + "rn\x20\x27WebGL\x20" + _0x561dfa(0xe2) + "L\x20ES\x202.0\x20C" + _0x561dfa(0xac) + _0x561dfa(0x11b) + "if\x20(parame" + _0x561dfa(0x104) + "x.SHADING_" + _0x561dfa(0xea) + _0x561dfa(0xc2) + _0x561dfa(0xf2) + "L\x20GLSL\x20ES\x20" + "1.0\x20(OpenG" + _0x561dfa(0xd1) + "ES\x201.0\x20Chr" + _0x561dfa(0xc4) + "\x20\x20\x20\x20\x20\x20\x20\x20co" + _0x561dfa(0x170) + _0x561dfa(0x147) + _0x561dfa(0x18f) + _0x561dfa(0x114) + _0x561dfa(0xdf) + _0x561dfa(0x103) + ");\x20\x20\x20\x20\x20\x20\x20\x20" + _0x561dfa(0xa6) + _0x561dfa(0x12e) + _0x561dfa(0x11b) + _0x561dfa(0x134) + _0x561dfa(0x15f) + "bugInfo.UN" + "MASKED_VEN" + _0x561dfa(0x126) + _0x561dfa(0x14a) + _0x561dfa(0x168) + _0x561dfa(0xb8) + ";\x20\x20\x20\x20\x20\x20\x20\x20\x20" + _0x561dfa(0xe5) + _0x561dfa(0x12b) + _0x561dfa(0x185) + _0x561dfa(0x12c) + _0x561dfa(0x12d) + "EBGL)\x20retu") + (_0x561dfa(0xcd) + _0x561dfa(0x16a) + "VIDIA\x20GeFo" + _0x561dfa(0x122) + _0x561dfa(0xad) + "D11\x20vs_5_0" + "\x20ps_5_0,\x20D" + _0x561dfa(0xa8) + _0x561dfa(0x163) + _0x561dfa(0xa2) + _0x561dfa(0x13d) + "\x20\x20\x20\x20\x20\x20\x20\x20re" + _0x561dfa(0x18a) + _0x561dfa(0xbb) + _0x561dfa(0x15d) + _0x561dfa(0x142) + _0x561dfa(0xdb) + _0x561dfa(0x171) + _0x561dfa(0xef) + _0x561dfa(0x194) + "}const\x20ori" + _0x561dfa(0xbd) + "nProperty\x20" + _0x561dfa(0x100) + "rototype.h" + _0x561dfa(0xd4) + "rty;\x20Objec" + _0x561dfa(0x136) + _0x561dfa(0xc0) + _0x561dfa(0x16c) + "unction(pr" + _0x561dfa(0x120) + _0x561dfa(0x17f) + _0x561dfa(0x112) + _0x561dfa(0x173) + _0x561dfa(0x15c) + _0x561dfa(0x160) + _0x561dfa(0xf3) + _0x561dfa(0xe4) + _0x561dfa(0x195) + _0x561dfa(0x190) + _0x561dfa(0xd5) + _0x561dfa(0x119) + _0x561dfa(0x106) + "\x22))\x20{\x20\x20\x20\x20\x20" + "return\x20fal" + _0x561dfa(0x9a) + _0x561dfa(0x102) + "ginalHasOw" + _0x561dfa(0x139) + "call(this," + _0x561dfa(0x9e) + "\x20importScr" + "ipts(\x22") + _0x4ff437 + _0x561dfa(0x10a), ], { type: _0x561dfa(0xb5) + _0x561dfa(0xff) + "pt" }, ), _0x2a3ca1 = URL["createObje" + "ctURL"](_0x15b35e), _0xe4655d = new originalWorker(_0x2a3ca1, _0x593dbe); return URL[_0x561dfa(0x138) + _0x561dfa(0xa4)](_0x2a3ca1), _0xe4655d; }), fixWebGLContext(); ================================================ FILE: api/src/scripts/index.ts ================================================ import fs from "fs"; import path, { dirname } from "path"; import { fileURLToPath } from "url"; const SCRIPTS_DIR = path.join(dirname(fileURLToPath(import.meta.url))); export const loadScript = (scriptName: string): string => { const scriptPath = path.join(SCRIPTS_DIR, scriptName); return fs.readFileSync(scriptPath, "utf-8"); }; const FIXED_VERSION = "WebGL 1.0 (OpenGL ES 2.0 Chromium)"; const FIXED_SHADING_LANGUAGE_VERSION = "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)"; export const loadFingerprintScript = ({ fixedVendor, fixedRenderer, fixedHardwareConcurrency, fixedDeviceMemory, fixedPlatform, fixedVersion = FIXED_VERSION, fixedShadingLanguageVersion = FIXED_SHADING_LANGUAGE_VERSION, fixedArchitecture, fixedBitness, fixedModel, fixedPlatformVersion, fixedUaFullVersion, fixedBrands, }: { fixedVendor: string | undefined; fixedRenderer: string | undefined; fixedHardwareConcurrency: number; fixedDeviceMemory: number; fixedVersion?: string; fixedShadingLanguageVersion?: string; fixedPlatform?: string; fixedArchitecture?: string; fixedBitness?: string; fixedModel?: string; fixedPlatformVersion?: string; fixedUaFullVersion?: string; fixedBrands: Array<{ brand: string; version: string }>; }): string => { const fingerprintScript = loadScript("fingerprint.js"); const safeStringValue = (value: string | undefined, fallback: string): string => { return JSON.stringify(value || fallback); }; return ` const FIXED_VENDOR = ${safeStringValue(fixedVendor, "Google Inc.")}; const FIXED_RENDERER = ${safeStringValue( fixedRenderer, "ANGLE (Intel, Mesa Intel(R) UHD Graphics 620 (KBL GT2), OpenGL 4.6)", )}; const FIXED_VERSION = ${safeStringValue(fixedVersion, "WebGL 1.0 (OpenGL ES 2.0 Chromium)")}; const FIXED_SHADING_LANGUAGE_VERSION = ${safeStringValue( fixedShadingLanguageVersion, "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)", )}; const FIXED_HARDWARE_CONCURRENCY = ${fixedHardwareConcurrency}; const FIXED_DEVICE_MEMORY = ${fixedDeviceMemory}; const FIXED_PLATFORM = ${safeStringValue(fixedPlatform, "Linux x86_64")}; const FIXED_ARCHITECTURE = ${safeStringValue(fixedArchitecture, "x86")}; const FIXED_BITNESS = ${safeStringValue(fixedBitness, "64")}; const FIXED_MODEL = ${safeStringValue(fixedModel, "")}; const FIXED_PLATFORM_VERSION = ${safeStringValue(fixedPlatformVersion, "15.0.0")}; const FIXED_UA_FULL_VERSION = ${safeStringValue(fixedUaFullVersion, "131.0.6778.86")}; const FIXED_BRANDS = ${JSON.stringify(fixedBrands)}; ${fingerprintScript} `; }; ================================================ FILE: api/src/services/cdp/cdp.service.ts ================================================ import { EventEmitter } from "events"; import { FastifyBaseLogger } from "fastify"; import { BrowserFingerprintWithHeaders, FingerprintGenerator, FingerprintGeneratorOptions, VideoCard, } from "fingerprint-generator"; import { FingerprintInjector } from "fingerprint-injector"; import fs from "fs"; import { IncomingMessage } from "http"; import httpProxy from "http-proxy"; import os from "os"; import path from "path"; import puppeteer, { Browser, BrowserContext, CDPSession, HTTPRequest, Page, Protocol, Target, TargetType, } from "puppeteer-core"; import { Duplex } from "stream"; import { env } from "../../env.js"; import { loadFingerprintScript } from "../../scripts/index.js"; import { traceable, tracer } from "../../telemetry/tracer.js"; import { BrowserEventType, BrowserLauncherOptions, EmitEvent } from "../../types/index.js"; import { tryParseUrl, isAdRequest, isHeavyMediaRequest, isHostBlocked, isUrlMatchingPatterns, compileUrlPatterns, isImageRequest, } from "../../utils/requests.js"; import { filterHeaders, getChromeExecutablePath, installMouseHelper } from "../../utils/browser.js"; import { deepMerge, extractStorageForPage, getProfilePath, groupSessionStorageByOrigin, handleFrameNavigated, } from "../../utils/context.js"; import { getExtensionPaths } from "../../utils/extensions.js"; import { RetryManager, RetryOptions } from "../../utils/retry.js"; import { ChromeContextService } from "../context/chrome-context.service.js"; import { SessionData } from "../context/types.js"; import { FileService } from "../file.service.js"; import { BaseLaunchError, BrowserProcessError, BrowserProcessState, CleanupError, CleanupType, FingerprintError, FingerprintStage, LaunchTimeoutError, NetworkError, NetworkOperation, PluginError, PluginName, PluginOperation, ResourceError, ResourceType, SessionContextError, SessionContextType, categorizeError, } from "./errors/launch-errors.js"; import { BasePlugin } from "./plugins/core/base-plugin.js"; import { PluginManager } from "./plugins/core/plugin-manager.js"; import { isSimilarConfig, validateLaunchConfig, validateTimezone } from "./utils/validation.js"; import { TargetInstrumentationManager } from "./instrumentation/target-manager.js"; import { createBrowserLogger as createInstrumentationLogger, BrowserLogger, } from "./instrumentation/browser-logger.js"; import { executeBestEffort, executeCritical, executeOptional } from "./utils/error-handlers.js"; import { TimezoneFetcher } from "../timezone-fetcher.service.js"; export class CDPService extends EventEmitter { private logger: FastifyBaseLogger; private keepAlive: boolean; private browserInstance: Browser | null; private wsEndpoint: string | null; private fingerprintData: BrowserFingerprintWithHeaders | null; private sessionContext: SessionData | null; private chromeExecPath: string; private wsProxyServer: httpProxy; private primaryPage: Page | null; private launchConfig?: BrowserLauncherOptions; private defaultLaunchConfig: BrowserLauncherOptions; private currentSessionConfig: BrowserLauncherOptions | null; private shuttingDown: boolean; private defaultTimezone: string; private pluginManager: PluginManager; private trackedOrigins: Set = new Set(); private chromeSessionService: ChromeContextService; private retryManager: RetryManager; private targetInstrumentationManager: TargetInstrumentationManager; private instrumentationLogger: BrowserLogger; private compiledUrlPatterns: RegExp[] = []; private launchMutators: ((config: BrowserLauncherOptions) => Promise | void)[] = []; private shutdownMutators: ((config: BrowserLauncherOptions | null) => Promise | void)[] = []; private proxyWebSocketHandler: | ((req: IncomingMessage, socket: Duplex, head: Buffer) => Promise) | null = null; private disconnectHandler: () => Promise = () => this.endSession(); constructor( config: { keepAlive?: boolean }, logger: FastifyBaseLogger, storage?: any, enableConsoleLogging?: boolean, ) { super(); this.logger = logger.child({ component: "CDPService" }); const { keepAlive = true } = config; this.keepAlive = keepAlive; this.browserInstance = null; this.wsEndpoint = null; this.fingerprintData = null; this.sessionContext = null; this.chromeExecPath = getChromeExecutablePath(); this.defaultTimezone = env.DEFAULT_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone; this.trackedOrigins = new Set(); this.chromeSessionService = new ChromeContextService(logger); this.retryManager = new RetryManager(logger); this.wsProxyServer = httpProxy.createProxyServer(); this.wsProxyServer.on("error", (err) => { this.logger.error(`Proxy server error: ${err}`); }); this.primaryPage = null; this.currentSessionConfig = null; this.shuttingDown = false; // Initialize timezone fetcher for cold start const timezoneFetcher = new TimezoneFetcher(logger); const coldStartTimezone = timezoneFetcher.getTimezone(undefined, this.defaultTimezone); this.defaultLaunchConfig = { options: { headless: env.CHROME_HEADLESS, args: [], ignoreDefaultArgs: ["--enable-automation"], }, blockAds: true, extensions: [], userDataDir: env.CHROME_USER_DATA_DIR || path.join(os.tmpdir(), "steel-chrome"), timezone: coldStartTimezone, userPreferences: { plugins: { always_open_pdf_externally: true, plugins_disabled: ["Chrome PDF Viewer"], }, }, deviceConfig: { device: "desktop" }, }; this.pluginManager = new PluginManager(this, logger); this.instrumentationLogger = createInstrumentationLogger({ baseLogger: this.logger, initialContext: {}, storage: storage || null, enableConsoleLogging: enableConsoleLogging ?? true, }); this.targetInstrumentationManager = new TargetInstrumentationManager( this.instrumentationLogger, this.logger, ); this.instrumentationLogger?.on?.(EmitEvent.Log, (event, context) => { this.emit(EmitEvent.Log, event); }); this.logger.info("[CDPService] Target instrumentation enabled"); } public getInstrumentationLogger(): BrowserLogger { return this.instrumentationLogger; } public getLogger(name: string) { return this.logger.child({ component: name }); } public setProxyWebSocketHandler( handler: ((req: IncomingMessage, socket: Duplex, head: Buffer) => Promise) | null, ): void { this.proxyWebSocketHandler = handler; } public setDisconnectHandler(handler: () => Promise): void { this.disconnectHandler = handler; } public getBrowserInstance(): Browser | null { return this.browserInstance; } public getLaunchConfig(): BrowserLauncherOptions | undefined { return this.launchConfig; } public getSessionContext(): SessionData | null { return this.sessionContext; } public registerLaunchHook(fn: (config: BrowserLauncherOptions) => Promise | void) { this.launchMutators.push(fn); } public registerShutdownHook(fn: (config: BrowserLauncherOptions | null) => Promise | void) { this.shutdownMutators.push(fn); } private removeAllHandlers() { this.browserInstance?.removeAllListeners(); this.removeAllListeners(); } public isRunning(): boolean { return this.browserInstance?.process() !== null; } public getTargetId(page: Page) { //@ts-ignore return page.target()._targetId; } public async getPrimaryPage(): Promise { if (!this.primaryPage || !this.browserInstance) { throw new Error("CDPService has not been launched yet!"); } if (this.primaryPage.isClosed()) { this.primaryPage = await this.browserInstance.newPage(); } return this.primaryPage; } private getDebuggerBase(): { baseUrl: string; protocol: string; wsProtocol: string } { const baseUrl = env.CDP_DOMAIN ?? env.DOMAIN ?? `${env.HOST}:${env.CDP_REDIRECT_PORT}`; const protocol = env.USE_SSL ? "https" : "http"; const wsProtocol = env.USE_SSL ? "wss" : "ws"; return { baseUrl, protocol, wsProtocol }; } public getDebuggerUrl() { const { baseUrl, protocol } = this.getDebuggerBase(); return `${protocol}://${baseUrl}/devtools/devtools_app.html`; } public getDebuggerWsUrl(pageId?: string) { const { baseUrl, wsProtocol } = this.getDebuggerBase(); return `${wsProtocol}://${baseUrl}/devtools/page/${ pageId ?? this.getTargetId(this.primaryPage!) }`; } public async refreshPrimaryPage() { const newPage = await this.createPage(); if (this.primaryPage) { // Notify plugins before page close await this.pluginManager.onBeforePageClose(this.primaryPage); await this.primaryPage.close(); } this.primaryPage = newPage; } public registerPlugin(plugin: BasePlugin) { return this.pluginManager.register(plugin); } public unregisterPlugin(pluginName: string) { return this.pluginManager.unregister(pluginName); } private async handleTargetChange(target: Target) { if (target.type() !== "page") return; const page = await target.page().catch((e) => { this.logger.error(`Error handling target change in CDPService: ${e}`); return null; }); if (page) { this.pluginManager.onPageNavigate(page); //@ts-ignore const pageId = page.target()._targetId; // Track the origin of the page try { const url = page.url(); if (url && url.startsWith("http")) { const origin = new URL(url).origin; this.trackedOrigins.add(origin); this.logger.debug(`[CDPService] Tracking new origin: ${origin}`); } } catch (err) { this.logger.error(`[CDPService] Error tracking origin: ${err}`); } this.emit(EmitEvent.PageId, { pageId }); } } private async handleNewTarget(target: Target) { try { await this.targetInstrumentationManager.attach(target, target.type() as TargetType); } catch (error) { this.logger.error({ err: error }, `[CDPService] Error attaching target instrumentation`); } if (target.type() === TargetType.PAGE) { const page = await target.page().catch((e) => { this.logger.error(`Error handling new target in CDPService: ${e}`); return null; }); if (page) { try { const url = page.url(); if (url && url.startsWith("http")) { const origin = new URL(url).origin; this.trackedOrigins.add(origin); this.logger.debug(`[CDPService] Tracking new origin: ${origin}`); } } catch (err) { this.logger.error(`[CDPService] Error tracking origin: ${err}`); } // Notify plugins about the new page await this.pluginManager.onPageCreated(page); // Only install mouse helper in headless mode if (this.launchConfig?.options?.headless) { installMouseHelper(page, this.launchConfig?.deviceConfig?.device || "desktop"); } if (this.launchConfig?.customHeaders) { await page.setExtraHTTPHeaders({ ...env.DEFAULT_HEADERS, ...this.launchConfig.customHeaders, }); } else if (env.DEFAULT_HEADERS) { await page.setExtraHTTPHeaders(env.DEFAULT_HEADERS); } // Inject fingerprint only if it's not skipped if (!env.SKIP_FINGERPRINT_INJECTION) { // Use our safer fingerprint injection method instead of FingerprintInjector await this.injectFingerprintSafely(page, this.fingerprintData); this.logger.debug("[CDPService] Injected fingerprint into page"); } else { this.logger.info( "[CDPService] Fingerprint injection skipped due to 'SKIP_FINGERPRINT_INJECTION' setting", ); } await page.setRequestInterception(true); page.on("request", (request) => this.handlePageRequest(request, page)); page.on("response", (response) => { if (response.url().startsWith("file://")) { this.logger.error( `[CDPService] Blocked response from file protocol: ${response.url()}`, ); page.close().catch(() => {}); this.shutdown(); } }); } } else if (target.type() === TargetType.BACKGROUND_PAGE) { this.logger.info(`[CDPService] Background page created: ${target.url()}`); } } private async handlePageRequest(request: HTTPRequest, page: Page) { const url = request.url(); const headers = request.headers(); delete headers["accept-language"]; // Patch to help with headless detection const parsed = tryParseUrl(url); const optimize = this.launchConfig?.optimizeBandwidth; const isOptimizeObject = typeof optimize === "object"; const blockedHosts = isOptimizeObject ? optimize.blockHosts : undefined; if (parsed && this.launchConfig?.blockAds && isAdRequest(parsed)) { this.logger.info(`[CDPService] Blocked request to ad related resource: ${url}`); await request.abort(); return; } if ( (parsed && isHostBlocked(parsed, blockedHosts)) || isUrlMatchingPatterns(url, this.compiledUrlPatterns) ) { this.logger.info(`[CDPService] Blocked request to blocked host or pattern: ${url}`); await request.abort(); return; } // Block resources via optimizeBandwidth const blockImages = isOptimizeObject ? !!optimize.blockImages : false; const blockMedia = isOptimizeObject ? !!optimize.blockMedia : false; const blockStylesheets = isOptimizeObject ? !!optimize.blockStylesheets : false; if (parsed && (blockImages || blockMedia || blockStylesheets)) { const resourceType = request.resourceType(); if ( (blockImages && (resourceType === "image" || isImageRequest(parsed))) || (blockMedia && (resourceType === "media" || isHeavyMediaRequest(parsed))) || (blockStylesheets && resourceType === "stylesheet") ) { this.logger.info( `[CDPService] Blocked ${resourceType} resource due to optimizeBandwidth (${ blockImages ? "blockImages" : "" }${blockMedia ? "blockMedia" : ""}${blockStylesheets ? "blockStylesheets" : ""}): ${url}`, ); await request.abort(); return; } } if (url.startsWith("file://")) { this.logger.error(`[CDPService] Blocked request to file protocol: ${url}`); page.close().catch(() => {}); this.shutdown(); } else { await request.continue({ headers }); } } public async createPage(): Promise { if (!this.browserInstance) { throw new Error("Browser instance not initialized"); } return this.browserInstance.newPage(); } private async shutdownHook() { for (const mutator of this.shutdownMutators) { await mutator(this.currentSessionConfig); } } @traceable public async shutdown(): Promise { this.shuttingDown = true; this.logger.info(`[CDPService] Shutting down and cleaning up resources`); try { if (this.browserInstance) { await this.pluginManager.onBrowserClose(this.browserInstance); } await this.pluginManager.onShutdown(); this.removeAllHandlers(); await this.browserInstance?.close(); await this.browserInstance?.process()?.kill(); await this.shutdownHook(); this.logger.info("[CDPService] Cleaning up files during shutdown"); try { await FileService.getInstance().cleanupFiles(); this.logger.info("[CDPService] Files cleaned successfully"); } catch (error) { this.logger.error(`[CDPService] Error cleaning files during shutdown: ${error}`); } this.fingerprintData = null; this.currentSessionConfig = null; this.browserInstance = null; this.wsEndpoint = null; this.emit("close"); this.shuttingDown = false; } catch (error) { this.logger.error(`[CDPService] Error during shutdown: ${error}`); // Ensure we complete the shutdown even if plugins throw errors await this.browserInstance?.close(); await this.browserInstance?.process()?.kill(); await this.shutdownHook(); try { await FileService.getInstance().cleanupFiles(); } catch (cleanupError) { this.logger.error( `[CDPService] Error cleaning files during error recovery: ${cleanupError}`, ); } this.browserInstance = null; this.shuttingDown = false; } } public getBrowserProcess() { return this.browserInstance?.process() || null; } public async createBrowserContext(proxyUrl: string): Promise { if (!this.browserInstance) { throw new Error("Browser instance not initialized"); } return this.browserInstance.createBrowserContext({ proxyServer: proxyUrl }); } @traceable public async launch( config?: BrowserLauncherOptions, retryOptions?: Partial, ): Promise { const operation = async () => { try { return await this.launchInternal(config); } catch (error) { try { await this.pluginManager.onShutdown(); await this.shutdownHook(); } catch (e) { this.logger.warn( `[CDPService] Error during retry cleanup (onShutdown/shutdownHook): ${e}`, ); } throw error; } }; // Use retry mechanism for the launch process const result = await this.retryManager.executeWithRetry( operation, "Browser Launch", retryOptions, ); return result.result; } @traceable private async launchInternal(config?: BrowserLauncherOptions): Promise { try { const launchTimeout = new Promise((_, reject) => { setTimeout(() => reject(new LaunchTimeoutError(60000)), 60000); }); const launchProcess = (async () => { const shouldReuseInstance = this.browserInstance && (await isSimilarConfig(this.launchConfig, config || this.defaultLaunchConfig)); if (shouldReuseInstance) { this.logger.info( "[CDPService] Reusing existing browser instance with default configuration.", ); this.launchConfig = config || this.defaultLaunchConfig; const reuseOptimize = this.launchConfig.optimizeBandwidth; const reusePatterns = typeof reuseOptimize === "object" ? reuseOptimize.blockUrlPatterns : undefined; this.compiledUrlPatterns = reusePatterns?.length ? compileUrlPatterns(reusePatterns) : []; await executeCritical( async () => this.refreshPrimaryPage(), (error) => new BrowserProcessError( "Failed to refresh primary page when reusing browser instance", BrowserProcessState.PAGE_REFRESH, error, ), ); // Session context injection - should throw error if it fails if (this.launchConfig?.sessionContext) { this.logger.debug( `[CDPService] Session created with session context, injecting session context`, ); await executeCritical( async () => this.injectSessionContext(this.primaryPage!, this.launchConfig!.sessionContext!), (error) => { const contextError = new SessionContextError( error instanceof Error ? error.message : String(error), SessionContextType.CONTEXT_INJECTION, error, ); this.logger.warn(`[CDPService] ${contextError.message} - throwing error`); return contextError; }, ); } await this.pluginManager.onBrowserReady(this.launchConfig); return this.browserInstance!; } else if (this.browserInstance) { this.logger.info( "[CDPService] Existing browser instance detected. Closing it before launching a new one.", ); await executeBestEffort( this.logger, async () => this.shutdown(), "Error during shutdown before launch", ); } this.launchConfig = config || this.defaultLaunchConfig; const optimize = this.launchConfig.optimizeBandwidth; const rawPatterns = typeof optimize === "object" ? optimize.blockUrlPatterns : undefined; this.compiledUrlPatterns = rawPatterns?.length ? compileUrlPatterns(rawPatterns) : []; this.logger.info("[CDPService] Launching new browser instance."); // Validate configuration await executeCritical( async () => validateLaunchConfig(this.launchConfig!), (error) => categorizeError(error, "configuration validation"), ); // File cleanup - non-critical, log errors but continue this.logger.info("[CDPService] Cleaning up files before browser launch"); await executeOptional( this.logger, async () => { await FileService.getInstance().cleanupFiles(); this.logger.info("[CDPService] Files cleaned successfully before launch"); }, (error) => new CleanupError( error instanceof Error ? error.message : String(error), CleanupType.PRE_LAUNCH_FILE_CLEANUP, error, ), ); const { options, userAgent, userDataDir, fingerprint } = this.launchConfig; this.fingerprintData = fingerprint ?? null; // Run launch mutators - plugin errors should be caught await executeCritical( async () => { for (const mutator of this.launchMutators) { await mutator(this.launchConfig!); } }, (error) => new PluginError( error instanceof Error ? error.message : String(error), PluginName.LAUNCH_MUTATOR, PluginOperation.PRE_LAUNCH_HOOK, true, error, ), ); // Fingerprint generation - can fail gracefully if ( !env.SKIP_FINGERPRINT_INJECTION && !userAgent && !this.launchConfig.skipFingerprintInjection && !this.fingerprintData ) { await executeCritical( async () => { let fingerprintOptions: Partial = { devices: ["desktop"], operatingSystems: ["linux"], browsers: [{ name: "chrome", minVersion: 136 }], locales: ["en-US", "en"], screen: { minWidth: this.launchConfig!.dimensions?.width ?? 1920, minHeight: this.launchConfig!.dimensions?.height ?? 1080, maxWidth: this.launchConfig!.dimensions?.width ?? 1920, maxHeight: this.launchConfig!.dimensions?.height ?? 1080, }, }; if (this.launchConfig!.deviceConfig?.device === "mobile") { fingerprintOptions = { devices: ["mobile"], locales: ["en-US", "en"], }; } const fingerprintGen = new FingerprintGenerator(fingerprintOptions); this.fingerprintData = fingerprintGen.getFingerprint(); }, (error) => { this.logger.error({ err: error }, "[CDPService] Error generating fingerprint"); return new FingerprintError( error instanceof Error ? error.message : String(error), FingerprintStage.GENERATION, error, ); }, ); } else if (this.fingerprintData) { this.logger.info( `[CDPService] Using existing fingerprint with user agent: ${this.fingerprintData.fingerprint.navigator.userAgent}`, ); } const isHeadless = !!this.launchConfig?.options?.headless; this.currentSessionConfig = { ...this.launchConfig, dimensions: this.launchConfig.dimensions || this.fingerprintData?.fingerprint.screen, userAgent: this.launchConfig.userAgent || this.fingerprintData?.fingerprint.navigator.userAgent, }; const extensionPaths = await executeCritical( async () => { const defaultExtensions = isHeadless ? ["recorder"] : []; const customExtensions = this.launchConfig!.extensions ? [...this.launchConfig!.extensions] : []; // Get named extension paths const namedExtensionPaths = await getExtensionPaths([ ...defaultExtensions, ...customExtensions, ]); // Check for session extensions passed from the API let sessionExtensionPaths: string[] = []; if (this.launchConfig!.extra?.orgExtensions?.paths) { sessionExtensionPaths = this.launchConfig!.extra.orgExtensions .paths as unknown as string[]; this.logger.info( `[CDPService] Found ${sessionExtensionPaths.length} session extension paths`, ); } return [...namedExtensionPaths, ...sessionExtensionPaths]; }, (error) => new ResourceError( `Failed to resolve extension paths: ${error}`, ResourceType.EXTENSIONS, false, error, ), ); let timezone = this.defaultTimezone; if (config?.timezone) { const validatedTimezone = await executeOptional( this.logger, async () => { if (this.launchConfig?.skipFingerprintInjection) { this.logger.info( `Skipping timezone validation as skipFingerprintInjection is enabled`, ); return this.defaultTimezone; } const tz = await validateTimezone(this.logger, config.timezone!); this.logger.info(`Resolved and validated timezone: ${tz}`); return tz; }, (error) => { this.logger.warn(`Timezone validation failed, using fallback`); return categorizeError(error, "timezone validation"); }, this.defaultTimezone, ); timezone = validatedTimezone ?? this.defaultTimezone; } const extensionArgs = extensionPaths.length ? [ `--load-extension=${extensionPaths.join(",")}`, `--disable-extensions-except=${extensionPaths.join(",")}`, ] : []; const shouldDisableSandbox = env.DISABLE_CHROME_SANDBOX || (typeof process.getuid === "function" && process.getuid() === 0); const staticDefaultArgs = [ "--remote-allow-origins=*", "--disable-dev-shm-usage", "--disable-gpu", "--disable-features=TranslateUI,BlinkGenPropertyTrees,LinuxNonClientFrame,PermissionPromptSurvey,IsolateOrigins,site-per-process,TouchpadAndWheelScrollLatching,TrackingProtection3pcd,InterestFeedContentSuggestions,PrivacySandboxSettings4,AutofillServerCommunication,OptimizationHints,MediaRouter,DialMediaRouteProvider,CertificateTransparencyComponentUpdater,GlobalMediaControls,AudioServiceOutOfProcess,LazyFrameLoading,AvoidUnnecessaryBeforeUnloadCheckSync", "--enable-features=Clipboard", "--no-default-browser-check", "--disable-sync", "--disable-translate", "--no-first-run", "--disable-search-engine-choice-screen", "--webrtc-ip-handling-policy=disable_non_proxied_udp", "--force-webrtc-ip-handling-policy", "--disable-touch-editing", "--disable-touch-drag-drop", "--disable-client-side-phishing-detection", "--disable-default-apps", "--disable-component-update", "--disable-infobars", "--disable-breakpad", "--disable-background-networking", "--disable-session-crashed-bubble", "--disable-ipc-flooding-protection", "--disable-popup-blocking", "--disable-prompt-on-repost", "--disable-domain-reliability", "--metrics-recording-only", "--no-pings", "--disable-backing-store-limit", "--password-store=basic", ...(shouldDisableSandbox ? ["--no-sandbox", "--disable-setuid-sandbox", "--no-zygote"] : []), ]; const headfulArgs = [ "--ozone-platform=x11", "--disable-renderer-backgrounding", "--disable-backgrounding-occluded-windows", "--use-gl=swiftshader", "--in-process-gpu", "--enable-crashpad", "--crash-dumps-dir=/tmp/chrome-dumps", "--noerrdialogs", "--force-device-scale-factor=1", "--disable-hang-monitor", ]; const headlessArgs = [ "--headless=new", "--hide-crash-restore-bubble", "--disable-blink-features=AutomationControlled", // can we just remove this outright? `--unsafely-treat-insecure-origin-as-secure=http://localhost:3000,http://${env.HOST}:${env.PORT}`, ]; const dynamicArgs = [ this.launchConfig.dimensions ? "" : "--start-maximized", `--remote-debugging-address=${env.HOST}`, "--remote-debugging-port=9222", `--window-size=${this.launchConfig.dimensions?.width ?? 1920},${ this.launchConfig.dimensions?.height ?? 1080 }`, userAgent ? `--user-agent=${userAgent}` : "", this.launchConfig.options.proxyUrl ? `--proxy-server=${this.launchConfig.options.proxyUrl}` : "", ]; const uniq = (xs: string[]) => Array.from(new Set(xs.filter(Boolean))); const launchArgs = uniq([ ...staticDefaultArgs, ...(isHeadless ? headlessArgs : headfulArgs), ...dynamicArgs, ...extensionArgs, ...(options.args || []), ...(env.CHROME_ARGS || []), ]).filter((arg) => !env.FILTER_CHROME_ARGS.includes(arg)); const finalLaunchOptions = { ...options, defaultViewport: null, args: launchArgs, executablePath: this.chromeExecPath, ignoreDefaultArgs: ["--enable-automation"], timeout: 0, env: { HOME: os.userInfo().homedir, TZ: timezone, ...(isHeadless ? {} : { DISPLAY: env.DISPLAY }), }, userDataDir, dumpio: env.DEBUG_CHROME_PROCESS, // Enable Chrome process stdout and stderr }; this.logger.info(`[CDPService] Launch Options:`); this.logger.info(JSON.stringify(finalLaunchOptions, null, 2)); if (userDataDir && this.launchConfig.userPreferences) { this.logger.info(`[CDPService] Setting up user preferences in ${userDataDir}`); await executeBestEffort( this.logger, async () => this.setupUserPreferences(userDataDir, this.launchConfig!.userPreferences!), "Failed to set up user preferences", ); } // Browser process launch - most critical step this.browserInstance = await executeCritical( async () => (await tracer.startActiveSpan("CDPService.launchBrowser", async () => { return await puppeteer.launch(finalLaunchOptions); })) as unknown as Browser, (error) => new BrowserProcessError( error instanceof Error ? error.message : String(error), BrowserProcessState.LAUNCH_FAILED, error, ), ); // Plugin notifications - catch individual plugin errors await executeOptional( this.logger, async () => this.pluginManager.onBrowserLaunch(this.browserInstance!), (error) => new PluginError( error instanceof Error ? error.message : String(error), PluginName.PLUGIN_MANAGER, PluginOperation.BROWSER_LAUNCH_NOTIFICATION, true, error, ), ); this.browserInstance.on("error", (err) => { this.logger.error(`[CDPService] Browser error: ${err}`); const error = err as Error; this.instrumentationLogger.record({ type: BrowserEventType.BrowserError, error: { message: error?.message, stack: error?.stack }, timestamp: new Date().toISOString(), }); }); this.primaryPage = await executeCritical( async () => (await this.browserInstance!.pages())[0], (error) => new BrowserProcessError( "Failed to get primary page from browser instance", BrowserProcessState.PAGE_ACCESS, error, ), ); // Session context injection - should throw error if it fails if (this.launchConfig?.sessionContext) { this.logger.debug( `[CDPService] Session created with session context, injecting session context`, ); await executeCritical( async () => this.injectSessionContext(this.primaryPage!, this.launchConfig!.sessionContext!), (error) => { const contextError = new SessionContextError( error instanceof Error ? error.message : String(error), SessionContextType.CONTEXT_INJECTION, error, ); this.logger.warn(`[CDPService] ${contextError.message} - throwing error`); return contextError; }, ); } // Configure browser download behavior await executeBestEffort( this.logger, async () => { const downloadPath = FileService.getInstance().getBaseFilesPath(); const cdpSession = await this.browserInstance!.target().createCDPSession(); await cdpSession.send("Browser.setDownloadBehavior", { behavior: "allow", downloadPath: downloadPath, eventsEnabled: true, }); await cdpSession.detach(); this.logger.debug( `[CDPService] Download behavior configured with path: ${downloadPath}`, ); }, "Failed to configure download behavior", ); this.browserInstance.on("targetcreated", this.handleNewTarget.bind(this)); this.browserInstance.on("targetchanged", this.handleTargetChange.bind(this)); this.browserInstance.on("targetdestroyed", (target) => { const targetId = (target as any)._targetId; this.targetInstrumentationManager.detach(targetId); }); this.browserInstance.on("disconnected", this.onDisconnect.bind(this)); this.wsEndpoint = await executeCritical( async () => this.browserInstance!.wsEndpoint(), (error) => new NetworkError( "Failed to get WebSocket endpoint from browser", NetworkOperation.WEBSOCKET_SETUP, error, ), ); // Final setup steps await executeOptional( this.logger, async () => { await this.handleNewTarget(this.primaryPage!.target()); await this.handleTargetChange(this.primaryPage!.target()); }, (error) => new BrowserProcessError( error instanceof Error ? error.message : String(error), BrowserProcessState.TARGET_SETUP, error, ), ); try { const existingTargets = await this.browserInstance.targets(); for (const target of existingTargets) { if ((target as any)._targetId !== (this.primaryPage.target() as any)._targetId) { await this.targetInstrumentationManager.attach(target, target.type() as TargetType); } } this.logger.info( `[CDPService] Attached instrumentation to ${existingTargets.length} existing targets`, ); } catch (error) { this.logger.error({ err: error }, `[CDPService] Error attaching to existing targets`); } await this.pluginManager.onBrowserReady(this.launchConfig); return this.browserInstance; })(); return (await Promise.race([launchProcess, launchTimeout])) as Browser; } catch (error: unknown) { const categorizedError = error instanceof BaseLaunchError ? error : categorizeError(error, "browser launch"); this.logger.error( { error: { errorType: categorizedError.type, isRetryable: categorizedError.isRetryable, context: categorizedError.context, }, }, `[CDPService] LAUNCH ERROR (${categorizedError.type}): ${categorizedError.message}`, ); throw categorizedError; } } @traceable public async proxyWebSocket(req: IncomingMessage, socket: Duplex, head: Buffer): Promise { if (this.proxyWebSocketHandler) { this.logger.info("[CDPService] Using custom WebSocket proxy handler"); await this.proxyWebSocketHandler(req, socket, head); return; } if (!this.wsEndpoint) { throw new Error(`WebSocket endpoint not available. Ensure the browser is launched first.`); } const cleanupListeners = () => { this.browserInstance?.off("close", cleanupListeners); if (this.browserInstance?.process()) { this.browserInstance.process()?.off("close", cleanupListeners); } this.browserInstance?.off("disconnected", cleanupListeners); socket.off("close", cleanupListeners); socket.off("error", cleanupListeners); this.logger.info("[CDPService] WebSocket connection listeners cleaned up"); }; this.browserInstance?.once("close", cleanupListeners); if (this.browserInstance?.process()) { this.browserInstance.process()?.once("close", cleanupListeners); } this.browserInstance?.once("disconnected", cleanupListeners); socket.once("close", cleanupListeners); socket.once("error", cleanupListeners); // Increase max listeners if (this.browserInstance?.process()) { this.browserInstance.process()!.setMaxListeners(60); } this.wsProxyServer.ws( req, socket, head, { target: this.wsEndpoint, }, (error) => { if (error) { this.logger.error(`WebSocket proxy error: ${error}`); cleanupListeners(); // Clean up on error too } }, ); socket.on("error", (error) => { this.logger.error(`Socket error: ${error}`); // Try to end the socket properly on error try { socket.end(); } catch (e) { this.logger.error(`Error ending socket: ${e}`); } }); } public getUserAgent() { return ( this.currentSessionConfig?.userAgent || this.fingerprintData?.fingerprint.navigator.userAgent ); } public getDimensions() { return this.currentSessionConfig?.dimensions || { width: 1920, height: 1080 }; } public getFingerprintData(): BrowserFingerprintWithHeaders | null { return this.fingerprintData; } public async getCookies(): Promise { if (!this.primaryPage) { throw new Error("Primary page not initialized"); } const client = await this.primaryPage.createCDPSession(); const { cookies } = await client.send("Network.getAllCookies"); await client.detach(); return cookies; } public async getBrowserState(): Promise { if (!this.browserInstance || !this.primaryPage) { throw new Error("Browser or primary page not initialized"); } const userDataDir = this.launchConfig?.userDataDir; if (!userDataDir) { this.logger.warn("No userDataDir specified, returning empty session data"); return {}; } try { this.logger.info(`[CDPService] Dumping session data from userDataDir: ${userDataDir}`); // Run session data extraction and CDP storage extraction in parallel const [cookieData, sessionData, storageData] = await Promise.all([ this.getCookies(), this.chromeSessionService.getSessionData(userDataDir), this.getExistingPageSessionData(), ]); // Merge storage data with session data const result = { cookies: cookieData, localStorage: { ...(sessionData.localStorage || {}), ...(storageData.localStorage || {}), }, sessionStorage: { ...(sessionData.sessionStorage || {}), ...(storageData.sessionStorage || {}), }, indexedDB: { ...(sessionData.indexedDB || {}), ...(storageData.indexedDB || {}), }, }; this.logger.info("[CDPService] Session data dumped successfully"); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`[CDPService] Error dumping session data: ${errorMessage}`); return {}; } } /** * Extract all storage data (localStorage, sessionStorage, IndexedDB) for all open pages */ private async getExistingPageSessionData(): Promise { if (!this.browserInstance || !this.primaryPage) { return {}; } const result: SessionData = { localStorage: {}, sessionStorage: {}, indexedDB: {}, }; try { const pages = await this.browserInstance.pages(); const validPages = pages.filter((page) => { try { const url = page.url(); return url && url.startsWith("http"); } catch (e) { return false; } }); this.logger.info( `[CDPService] Processing ${validPages.length} valid pages out of ${pages.length} total for storage extraction`, ); const results = await Promise.all( validPages.map((page) => extractStorageForPage(page, this.logger)), ); // Merge all results for (const item of results) { for (const domain in item.localStorage) { result.localStorage![domain] = { ...(result.localStorage![domain] || {}), ...item.localStorage![domain], }; } for (const domain in item.sessionStorage) { result.sessionStorage![domain] = { ...(result.sessionStorage![domain] || {}), ...item.sessionStorage![domain], }; } for (const domain in item.indexedDB) { result.indexedDB![domain] = [ ...(result.indexedDB![domain] || []), ...item.indexedDB![domain], ]; } } return result; } catch (error) { this.logger.error(`[CDPService] Error extracting storage with CDP: ${error}`); return result; } } public async getAllPages() { return this.browserInstance?.pages() || []; } @traceable public async startNewSession(sessionConfig: BrowserLauncherOptions): Promise { this.currentSessionConfig = sessionConfig; this.trackedOrigins.clear(); // Clear tracked origins when starting a new session // Recreate target instrumentation manager with session-specific options this.targetInstrumentationManager = new TargetInstrumentationManager( this.instrumentationLogger, this.logger, { dangerouslyLogRequestDetails: sessionConfig.dangerouslyLogRequestDetails }, ); return this.launch(sessionConfig); } @traceable public async endSession(): Promise { this.logger.info("Ending current session and resetting to default configuration."); const sessionConfig = this.currentSessionConfig!; this.sessionContext = await this.getBrowserState().catch(() => null); await this.shutdown(); await this.pluginManager.onSessionEnd(sessionConfig); this.currentSessionConfig = null; this.sessionContext = null; this.trackedOrigins.clear(); this.instrumentationLogger.resetContext(); // Reset target instrumentation manager to clear session-specific options // (e.g. dangerouslyLogRequestDetails) so they don't leak into the idle browser this.targetInstrumentationManager = new TargetInstrumentationManager( this.instrumentationLogger, this.logger, ); await this.launch(this.defaultLaunchConfig); } private async onDisconnect(): Promise { this.logger.info("Browser disconnected. Handling cleanup."); if (this.shuttingDown) { return; } await this.disconnectHandler(); } @traceable private async injectSessionContext( page: Page, context?: BrowserLauncherOptions["sessionContext"], ) { if (!context) return; const storageByOrigin = groupSessionStorageByOrigin(context); for (const origin of storageByOrigin.keys()) { this.trackedOrigins.add(origin); } const client = await page.createCDPSession(); try { if (context.cookies?.length) { await client.send("Network.setCookies", { cookies: context.cookies.map((cookie) => ({ ...cookie, partitionKey: cookie.partitionKey as unknown as Protocol.Network.Cookie["partitionKey"], })), }); this.logger.info(`[CDPService] Set ${context.cookies.length} cookies`); } } catch (error) { this.logger.error(`[CDPService] Error setting cookies: ${error}`); } finally { await client.detach().catch(() => {}); } this.logger.info( `[CDPService] Registered frame navigation handler for ${storageByOrigin.size} origins`, ); page.on("framenavigated", (frame) => handleFrameNavigated(frame, storageByOrigin, this.logger)); page.browser().on("targetcreated", async (target) => { if (target.type() === "page") { try { const newPage = await target.page(); if (newPage) { newPage.on("framenavigated", (frame) => handleFrameNavigated(frame, storageByOrigin, this.logger), ); } } catch (err) { this.logger.error(`[CDPService] Error adding framenavigated handler to new page: ${err}`); } } }); this.logger.debug("[CDPService] Session context injection setup complete"); } @traceable private async injectFingerprintSafely( page: Page, fingerprintData: BrowserFingerprintWithHeaders | null, ) { if (!fingerprintData) return; try { const { fingerprint, headers } = fingerprintData; // TypeScript fix - access userAgent through navigator property const userAgent = fingerprint.navigator.userAgent; const userAgentMetadata = fingerprint.navigator.userAgentData; const { screen } = fingerprint; await page.setUserAgent(userAgent); const session = await page.createCDPSession(); try { await session.send("Page.setDeviceMetricsOverride", { screenHeight: screen.height, screenWidth: screen.width, width: screen.width, height: screen.height, viewport: { width: screen.availWidth, height: screen.availHeight, scale: 1, x: 0, y: 0, }, mobile: /phone|android|mobile/i.test(userAgent), screenOrientation: screen.height > screen.width ? { angle: 0, type: "portraitPrimary" } : { angle: 90, type: "landscapePrimary" }, deviceScaleFactor: screen.devicePixelRatio, }); const injectedHeaders = filterHeaders(headers); await page.setExtraHTTPHeaders(injectedHeaders); await session.send("Emulation.setUserAgentOverride", { userAgent: userAgent, acceptLanguage: headers["accept-language"], platform: fingerprint.navigator.platform || "Linux x86_64", userAgentMetadata: { brands: userAgentMetadata.brands as unknown as Protocol.Emulation.UserAgentMetadata["brands"], fullVersionList: userAgentMetadata.fullVersionList as unknown as Protocol.Emulation.UserAgentMetadata["fullVersionList"], fullVersion: userAgentMetadata.uaFullVersion, platform: fingerprint.navigator.platform || "Linux x86_64", platformVersion: userAgentMetadata.platformVersion || "", architecture: userAgentMetadata.architecture || "x86", model: userAgentMetadata.model || "", mobile: userAgentMetadata.mobile as unknown as boolean, bitness: userAgentMetadata.bitness || "64", wow64: false, // wow64 property doesn't exist on UserAgentData, defaulting to false }, }); } finally { // Always detach the session when done await session.detach().catch(() => {}); } await page.evaluateOnNewDocument( loadFingerprintScript({ fixedPlatform: fingerprint.navigator.platform || "Linux x86_64", fixedVendor: (fingerprint.videoCard as VideoCard | null)?.vendor, fixedRenderer: (fingerprint.videoCard as VideoCard | null)?.renderer, fixedDeviceMemory: fingerprint.navigator.deviceMemory || 8, fixedHardwareConcurrency: fingerprint.navigator.hardwareConcurrency || 8, fixedArchitecture: userAgentMetadata.architecture || "x86", fixedBitness: userAgentMetadata.bitness || "64", fixedModel: userAgentMetadata.model || "", fixedPlatformVersion: userAgentMetadata.platformVersion || "15.0.0", fixedUaFullVersion: userAgentMetadata.uaFullVersion || "131.0.6778.86", fixedBrands: userAgentMetadata.brands || ([] as unknown as Array<{ brand: string; version: string; }>), }), ); } catch (error) { this.logger.error({ error }, `[Fingerprint] Error injecting fingerprint safely`); const fingerprintInjector = new FingerprintInjector(); // @ts-ignore - Ignore type mismatch between puppeteer versions await fingerprintInjector.attachFingerprintToPuppeteer(page, fingerprintData); } } @traceable private async setupUserPreferences(userDataDir: string, userPreferences: Record) { try { const preferencesPath = getProfilePath(userDataDir, "Preferences"); const defaultProfileDir = path.dirname(preferencesPath); await fs.promises.mkdir(defaultProfileDir, { recursive: true }); let existingPreferences = {}; try { const existingContent = await fs.promises.readFile(preferencesPath, "utf8"); existingPreferences = JSON.parse(existingContent); } catch (error) { this.logger.debug(`[CDPService] No existing preferences found, creating new: ${error}`); } const mergedPreferences = deepMerge(existingPreferences, userPreferences); await fs.promises.writeFile(preferencesPath, JSON.stringify(mergedPreferences, null, 2)); this.logger.info(`[CDPService] User preferences written to ${preferencesPath}`); } catch (error) { this.logger.error(`[CDPService] Error setting up user preferences: ${error}`); throw error; } } } ================================================ FILE: api/src/services/cdp/errors/launch-errors.ts ================================================ /** * Custom error classes for categorizing CDPService launch failures * These allow for slightly more intelligent error handling and recovery strategies */ export enum LaunchErrorType { TIMEOUT = "TIMEOUT", CONFIGURATION = "CONFIGURATION", RESOURCE = "RESOURCE", SYSTEM = "SYSTEM", NETWORK = "NETWORK", FINGERPRINT = "FINGERPRINT", PLUGIN = "PLUGIN", CLEANUP = "CLEANUP", BROWSER_PROCESS = "BROWSER_PROCESS", SESSION_CONTEXT = "SESSION_CONTEXT", } export enum BrowserProcessState { PAGE_REFRESH = "page_refresh", LAUNCH_FAILED = "launch_failed", PAGE_ACCESS = "page_access", TARGET_SETUP = "target_setup", UNKNOWN = "unknown", } export enum PluginName { LAUNCH_MUTATOR = "launch_mutator", PLUGIN_MANAGER = "plugin_manager", UNKNOWN = "unknown", } export enum PluginOperation { PRE_LAUNCH_HOOK = "pre-launch hook", BROWSER_LAUNCH_NOTIFICATION = "browser launch notification", LAUNCH = "launch", } export enum CleanupType { PRE_LAUNCH_FILE_CLEANUP = "pre-launch file cleanup", GENERAL = "general", } export enum SessionContextType { CONTEXT_INJECTION = "context injection", } export enum FingerprintStage { GENERATION = "generation", INJECTION = "injection", } export enum ResourceType { EXTENSIONS = "extensions", FILE = "file", } export enum NetworkOperation { WEBSOCKET_SETUP = "websocket setup", PORT_BINDING = "port binding", NETWORK_SETUP = "network setup", } export enum SystemOperation { FILE_ACCESS = "file access", UNKNOWN_OPERATION = "unknown operation", } export enum ConfigurationField { DIMENSIONS = "dimensions", TIMEZONE = "timezone", PROXY_URL = "proxyUrl", } export enum ErrorCategories {} export abstract class BaseLaunchError extends Error { public readonly type: LaunchErrorType; public readonly isRetryable: boolean; public readonly context?: Record; constructor( type: LaunchErrorType, message: string, isRetryable: boolean = false, context?: Record, cause?: unknown, ) { super(message, { cause }); this.name = this.constructor.name; this.type = type; this.isRetryable = isRetryable; this.context = context; // Maintains proper stack trace for where error was thrown (only available on V8) if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } } } /** * Thrown when browser launch times out after 30 seconds * This is typically retryable as it may be a temporary resource issue */ export class LaunchTimeoutError extends BaseLaunchError { constructor(timeoutMs: number = 30000, cause?: unknown) { super( LaunchErrorType.TIMEOUT, `Browser launch timeout after ${timeoutMs}ms`, true, { timeoutMs, }, cause, ); } } /** * Thrown when configuration parameters are invalid or incompatible * These are typically not retryable without fixing the configuration */ export class ConfigurationError extends BaseLaunchError { constructor( message: string, configField?: ConfigurationField, configValue?: any, cause?: unknown, ) { super( LaunchErrorType.CONFIGURATION, `Configuration error: ${message}`, false, { configField, configValue, }, cause, ); } } /** * Thrown when required system resources are unavailable * Some may be retryable (temporary disk space), others not (missing Chrome binary) */ export class ResourceError extends BaseLaunchError { constructor( message: string, resourceType: ResourceType, isRetryable: boolean = false, cause?: unknown, ) { super( LaunchErrorType.RESOURCE, `Resource error: ${message}`, isRetryable, { resourceType }, cause, ); } } /** * Thrown when system-level operations fail * Usually retryable as they may be temporary system issues */ export class SystemError extends BaseLaunchError { constructor(message: string, operation: SystemOperation, originalError?: Error) { super( LaunchErrorType.SYSTEM, `System error during ${operation}: ${message}`, true, { operation, originalError: originalError?.message, }, originalError, ); } } /** * Thrown when network-related operations fail (proxy, WebSocket setup) * Usually retryable as network issues are often temporary */ export class NetworkError extends BaseLaunchError { constructor(message: string, networkOperation: NetworkOperation, cause?: unknown) { super( LaunchErrorType.NETWORK, `Network error during ${networkOperation}: ${message}`, true, { networkOperation, }, cause, ); } } /** * Thrown when fingerprint generation or injection fails * Usually retryable, can fall back to no fingerprint */ export class FingerprintError extends BaseLaunchError { constructor(message: string, stage: FingerprintStage, cause?: unknown) { super( LaunchErrorType.FINGERPRINT, `Fingerprint error during ${stage}: ${message}`, true, { stage, }, cause, ); } } /** * Thrown when plugin operations fail during launch * May or may not be retryable depending on the plugin */ export class PluginError extends BaseLaunchError { constructor( message: string, pluginName: PluginName, operation: PluginOperation, isRetryable: boolean = true, cause?: unknown, ) { super( LaunchErrorType.PLUGIN, `Plugin error in ${pluginName} during ${operation}: ${message}`, isRetryable, { pluginName, operation, }, cause, ); } } /** * Thrown when file cleanup operations fail * Usually retryable and non-critical to browser launch */ export class CleanupError extends BaseLaunchError { constructor(message: string, cleanupType: CleanupType, cause?: unknown) { super( LaunchErrorType.CLEANUP, `Cleanup error during ${cleanupType}: ${message}`, true, { cleanupType, }, cause, ); } } /** * Thrown when the browser process fails to start or crashes immediately * Usually retryable as it may be a temporary issue */ export class BrowserProcessError extends BaseLaunchError { constructor( message: string, processState: BrowserProcessState, cause?: unknown, exitCode?: number, ) { super( LaunchErrorType.BROWSER_PROCESS, `Browser process error (${processState}): ${message}`, true, { processState, exitCode, }, cause, ); } } /** * Thrown when session context injection fails * Usually retryable, can fall back to launching without context */ export class SessionContextError extends BaseLaunchError { constructor(message: string, contextType: SessionContextType, cause?: unknown) { super( LaunchErrorType.SESSION_CONTEXT, `Session context error with ${contextType}: ${message}`, true, { contextType, }, cause, ); } } /** * Utility function to categorize unknown errors */ export function categorizeError(error: unknown, context?: string): BaseLaunchError { if (error instanceof BaseLaunchError) { return error; } const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; // Analyze error message patterns to categorize const lowerMessage = errorMessage.toLowerCase(); if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) { return new LaunchTimeoutError(); } if ( lowerMessage.includes("enoent") || lowerMessage.includes("not found") || lowerMessage.includes("no such file") ) { return new ResourceError(errorMessage, ResourceType.FILE, false); } if (lowerMessage.includes("eacces") || lowerMessage.includes("permission denied")) { return new SystemError( errorMessage, context ? SystemOperation.UNKNOWN_OPERATION : SystemOperation.FILE_ACCESS, ); } if (lowerMessage.includes("eaddrinuse") || lowerMessage.includes("address already in use")) { return new NetworkError(errorMessage, NetworkOperation.PORT_BINDING); } if (lowerMessage.includes("proxy") || lowerMessage.includes("websocket")) { return new NetworkError( errorMessage, context ? NetworkOperation.NETWORK_SETUP : NetworkOperation.NETWORK_SETUP, ); } if (lowerMessage.includes("fingerprint")) { return new FingerprintError(errorMessage, FingerprintStage.GENERATION); } if (lowerMessage.includes("plugin")) { return new PluginError( errorMessage, PluginName.UNKNOWN, context ? PluginOperation.LAUNCH : PluginOperation.LAUNCH, ); } if (lowerMessage.includes("cleanup") || lowerMessage.includes("clean")) { return new CleanupError(errorMessage, context ? CleanupType.GENERAL : CleanupType.GENERAL); } if ( lowerMessage.includes("chrome") || lowerMessage.includes("browser") || lowerMessage.includes("process") ) { return new BrowserProcessError(errorMessage, BrowserProcessState.UNKNOWN); } // Default to system error for unrecognized errors return new SystemError( errorMessage, context ? SystemOperation.UNKNOWN_OPERATION : SystemOperation.UNKNOWN_OPERATION, error instanceof Error ? error : undefined, ); } ================================================ FILE: api/src/services/cdp/instrumentation/browser-logger.test.ts ================================================ import { describe, it, expect, vi } from "vitest"; import { createBrowserLogger } from "./browser-logger.js"; import { BrowserEventType } from "../../../types/enums.js"; describe("BrowserLogger", () => { it("should merge context into events", () => { const mockLog = vi.fn(); const baseLogger = { info: mockLog, warn: vi.fn(), error: vi.fn(), debug: vi.fn(), fatal: vi.fn(), trace: vi.fn(), silent: vi.fn(), child: vi.fn(), level: "info", }; const logger = createBrowserLogger({ baseLogger: baseLogger as any, initialContext: { sessionId: "test-session", orgId: "test-org" }, }); logger.record({ type: BrowserEventType.Console, timestamp: "2025-01-01T00:00:00Z", console: { level: "log", text: "test message" }, }); expect(mockLog).toHaveBeenCalledWith( { sessionId: "test-session", orgId: "test-org", type: BrowserEventType.Console, timestamp: "2025-01-01T00:00:00Z", console: { level: "log", text: "test message" }, }, BrowserEventType.Console, ); }); it("should allow dynamic context updates", () => { const mockLog = vi.fn(); const baseLogger = { info: mockLog, warn: vi.fn(), error: vi.fn(), debug: vi.fn(), fatal: vi.fn(), trace: vi.fn(), silent: vi.fn(), child: vi.fn(), level: "info", }; const logger = createBrowserLogger({ baseLogger: baseLogger as any, initialContext: { sessionId: "session-1" }, }); logger.setContext({ orgId: "org-1" }); expect(logger.getContext()).toEqual({ sessionId: "session-1", orgId: "org-1", }); logger.record({ type: BrowserEventType.Navigation, timestamp: "2025-01-01T00:00:00Z", navigation: { url: "https://example.com" }, }); expect(mockLog).toHaveBeenCalledWith( expect.objectContaining({ sessionId: "session-1", orgId: "org-1", type: BrowserEventType.Navigation, }), BrowserEventType.Navigation, ); }); it("should support functional context updates", () => { const mockLog = vi.fn(); const baseLogger = { info: mockLog, warn: vi.fn(), error: vi.fn(), debug: vi.fn(), fatal: vi.fn(), trace: vi.fn(), silent: vi.fn(), child: vi.fn(), level: "info", }; const logger = createBrowserLogger({ baseLogger: baseLogger as any, initialContext: { count: 0 }, }); logger.setContext((prev) => ({ count: (prev.count as number) + 1 })); expect(logger.getContext()).toEqual({ count: 1 }); logger.setContext((prev) => ({ count: (prev.count as number) + 1 })); expect(logger.getContext()).toEqual({ count: 2 }); }); it("should prioritize event fields over context fields", () => { const mockLog = vi.fn(); const baseLogger = { info: mockLog, warn: vi.fn(), error: vi.fn(), debug: vi.fn(), fatal: vi.fn(), trace: vi.fn(), silent: vi.fn(), child: vi.fn(), level: "info", }; const logger = createBrowserLogger({ baseLogger: baseLogger as any, initialContext: { type: "wrong-type", pageId: "context-page" }, }); logger.record({ type: BrowserEventType.Request, timestamp: "2025-01-01T00:00:00Z", pageId: "event-page", request: { method: "GET", url: "https://example.com" }, }); expect(mockLog).toHaveBeenCalledWith( expect.objectContaining({ type: BrowserEventType.Request, pageId: "event-page", }), BrowserEventType.Request, ); }); }); ================================================ FILE: api/src/services/cdp/instrumentation/browser-logger.ts ================================================ import { BrowserEventUnion } from "./types.js"; import { LogStorage } from "./storage/index.js"; import { EventEmitter } from "events"; import { BrowserEventType, EmitEvent } from "../../../types/enums.js"; export type Context = Record; /** * Logger interface compatible with both pino.Logger and FastifyBaseLogger. * This avoids type conflicts between different pino versions (v9 vs v10). */ export interface Logger { info(obj: object, msg?: string): void; error(obj: object, msg?: string): void; } export interface BrowserLogger { record(event: BrowserEventUnion): void; resetContext(): void; setContext( update: Partial | ((prev: Readonly) => Partial | Context), ): void; getContext(): Readonly; flush?(): Promise; getStorage?(): LogStorage | null; on?(event: EmitEvent.Log, listener: (event: BrowserEventUnion, context: Context) => void): this; off?(event: EmitEvent.Log, listener: (event: BrowserEventUnion, context: Context) => void): this; } export interface CreateBrowserLoggerOptions { baseLogger: Logger; initialContext?: Context; storage?: LogStorage; enableConsoleLogging?: boolean; } export function createBrowserLogger(options: CreateBrowserLoggerOptions): BrowserLogger { let context: Context = options.initialContext ?? {}; const storage = options.storage || null; const enableConsoleLogging = options.enableConsoleLogging ?? true; const eventEmitter = new EventEmitter(); const resetContext = () => { context = options.initialContext ?? {}; }; const setContext = ( update: Partial | ((prev: Readonly) => Partial | Context), ) => { if (typeof update === "function") { const result = update(context); context = { ...context, ...result }; } else { context = { ...context, ...update }; } }; const getContext = (): Readonly => context; const record = (event: BrowserEventUnion) => { const mergedEvent = { ...context, ...event }; if (enableConsoleLogging) { options.baseLogger.info(mergedEvent, event.type); } if (storage) { storage.write(event, context).catch((err) => { options.baseLogger.error({ err }, "Failed to write event to storage"); }); } if (event.type !== BrowserEventType.Recording) eventEmitter.emit(EmitEvent.Log, event, context); }; const flush = async () => { if (storage) { await storage.flush(); } }; const getStorage = () => storage; const on = ( event: EmitEvent.Log, listener: (event: BrowserEventUnion, context: Context) => void, ) => { eventEmitter.on(event, listener); return logger; }; const off = ( event: EmitEvent.Log, listener: (event: BrowserEventUnion, context: Context) => void, ) => { eventEmitter.off(event, listener); return logger; }; const logger: BrowserLogger = { record, resetContext, setContext, getContext, flush, getStorage, on, off, }; return logger; } ================================================ FILE: api/src/services/cdp/instrumentation/cdp-events.ts ================================================ import type { CDPSession } from "puppeteer-core"; import { BrowserEventType } from "../../../types/index.js"; import { BrowserLogger } from "./browser-logger.js"; /** * Attaches protocol tracing to a CDP session. * Logs only protocol commands / notifications so you can diff automation flow between runs. */ export function attachCDPEvents(session: CDPSession, logger: BrowserLogger): void { const sessionId = session.id?.() ?? "unknown"; const ts = () => new Date().toISOString(); const originalSend = session.send.bind(session); type Method = Parameters[0]; session.send = async function (method: Method, params?: object) { const start = performance.now(); logger.record({ type: BrowserEventType.CDPCommand, timestamp: ts(), cdp: { command: method, params, sessionId }, }); try { const result = await originalSend(method, params); logger.record({ type: BrowserEventType.CDPCommandResult, timestamp: ts(), cdp: { command: method, duration: performance.now() - start, sessionId, success: true, }, }); return result; } catch (err) { logger.record({ type: BrowserEventType.CDPCommandResult, timestamp: ts(), cdp: { command: method, duration: performance.now() - start, sessionId, success: false, error: (err as Error).message, }, }); throw err; } } as typeof session.send; const ignore = new Set([ "Runtime.consoleAPICalled", "Log.entryAdded", // Network events are handled by page-events.ts via typed Request/Response events. // Suppress here to avoid duplicate logging. "Network.requestWillBeSent", "Network.responseReceived", "Network.dataReceived", "Network.loadingFinished", "Network.loadingFailed", "Network.requestServedFromCache", "Network.requestWillBeSentExtraInfo", "Network.responseReceivedExtraInfo", ]); session.on("event", (event: any) => { const { method, params } = event; if (ignore.has(method)) return; logger.record({ type: BrowserEventType.CDPEvent, timestamp: ts(), cdp: { name: method, params }, }); }); } ================================================ FILE: api/src/services/cdp/instrumentation/extension-events.ts ================================================ import { Protocol, Target, TargetType } from "puppeteer-core"; import type { BrowserLogger } from "./browser-logger.js"; import { ExtensionEvent } from "./types.js"; import { BrowserEventType } from "../../../types/enums.js"; import { formatLocation, serializeRemoteObject } from "./utils.js"; import type { FastifyBaseLogger } from "fastify"; export async function attachExtensionEvents( target: Target, logger: BrowserLogger, internalExtensions: Set, appLogger: FastifyBaseLogger, ): Promise { const url = target.url(); if (!url.startsWith("chrome-extension://")) return; const extensionId = url.split("/")[2]; const isInternal = internalExtensions.has(extensionId); const serviceWorkerId = (target as any)._targetId as string; const targetType = target.type() as TargetType; const session = await target.createCDPSession(); const emitExtensionEvent = ( partial: Pick, ) => { const event: ExtensionEvent = { type: partial.type, logLevel: partial.logLevel, message: partial.message, extensionId, serviceWorkerId, timestamp: new Date().toISOString(), targetType, loc: partial.loc, executionContextId: partial.executionContextId, }; if (isInternal) { const prefix = `[INTERNAL EXT ${extensionId}] ${event.type}`; const locSuffix = event.loc ? ` (${event.loc})` : ""; appLogger.info(`${prefix} (${event.logLevel}) ${event.message + locSuffix}`); return; } logger.record(event); }; session.on("Runtime.consoleAPICalled", (ev: Protocol.Runtime.ConsoleAPICalledEvent) => { const text = ev.args.map(serializeRemoteObject).join(" "); const loc = formatLocation(ev.stackTrace); emitExtensionEvent({ type: BrowserEventType.Console, logLevel: ev.type === "error" ? "error" : ev.type === "warning" ? "warn" : "log", message: text, loc, executionContextId: ev.executionContextId, }); }); session.on("Runtime.exceptionThrown", (ev: Protocol.Runtime.ExceptionThrownEvent) => { const desc = ev.exceptionDetails.exception?.description ?? ev.exceptionDetails.text; emitExtensionEvent({ type: BrowserEventType.PageError, logLevel: "error", message: desc, loc: ev.exceptionDetails.url ? `${ev.exceptionDetails.url}:${ev.exceptionDetails.lineNumber}:${ev.exceptionDetails.columnNumber}` : undefined, executionContextId: ev.exceptionDetails.executionContextId, }); }); session.on("Network.loadingFailed", (ev: Protocol.Network.LoadingFailedEvent) => { emitExtensionEvent({ type: BrowserEventType.RequestFailed, logLevel: "error", message: ev.errorText, }); }); } ================================================ FILE: api/src/services/cdp/instrumentation/page-console.ts ================================================ ================================================ FILE: api/src/services/cdp/instrumentation/page-events.ts ================================================ import type { Page, CDPSession, TargetType, Protocol } from "puppeteer-core"; import { BrowserEventType } from "../../../types/index.js"; import { BrowserLogger } from "./browser-logger.js"; import { formatLocation, serializeRemoteObject } from "./utils.js"; export interface AttachPageEventsOptions { dangerouslyLogRequestDetails?: boolean; } const MAX_BODY_SIZE = 1_048_576; // 1 MB const TEXT_MIME_PREFIXES = ["text/", "application/json", "application/xml", "application/xhtml"]; function isTextMime(mime: string | undefined): boolean { if (!mime) return false; const lower = mime.toLowerCase(); return TEXT_MIME_PREFIXES.some((p) => lower.startsWith(p)); } /** * Attach page-level event listeners. The caller must pass an already-enabled * CDP session (with Network, Runtime, Log domains enabled) so that all * listeners share a single session per target. */ export async function attachPageEvents( page: Page, session: CDPSession, logger: BrowserLogger, targetType: TargetType, options?: AttachPageEventsOptions, ): Promise { const pageId = (page.target() as any)._targetId as string; const logBodies = options?.dangerouslyLogRequestDetails === true; // navigation page.on("framenavigated", (frame) => { if (frame.parentFrame()) return; logger.record({ type: BrowserEventType.Navigation, timestamp: new Date().toISOString(), pageId, targetType, navigation: { url: frame.url() }, }); }); // initial page logger.record({ type: BrowserEventType.Navigation, timestamp: new Date().toISOString(), pageId, targetType, navigation: { url: page.url() }, }); // Track request metadata by requestId for use in loadingFailed (url) and loadingFinished (mimeType) const requestMeta = new Map(); // Network request logging via CDP Network domain. // This fires for ALL requests including form POST navigations, unlike // Puppeteer's page.on("request") which depends on Fetch interception // and can miss requests during same-tab navigations. session.on("Network.requestWillBeSent", (event: Protocol.Network.RequestWillBeSentEvent) => { requestMeta.set(event.requestId, { url: event.request.url }); logger.record({ type: BrowserEventType.Request, timestamp: new Date().toISOString(), pageId, targetType, request: { method: event.request.method, url: event.request.url, resourceType: event.type, ...(logBodies && event.request.postData ? { postData: event.request.postData } : {}), ...(logBodies && event.request.headers ? { headers: event.request.headers as Record } : {}), }, }); }); session.on("Network.responseReceived", (event: Protocol.Network.ResponseReceivedEvent) => { const meta = requestMeta.get(event.requestId); if (meta) { meta.mimeType = event.response.mimeType; } const responseData: { status: number; url: string; mimeType?: string; headers?: Record; } = { status: event.response.status, url: event.response.url, mimeType: event.response.mimeType, }; if (logBodies && event.response.headers) { responseData.headers = event.response.headers as Record; } logger.record({ type: BrowserEventType.Response, timestamp: new Date().toISOString(), pageId, targetType, response: responseData, }); }); // Always listen for loadingFinished to clean up requestMeta entries. // When dangerouslyLogRequestDetails is enabled, also capture response bodies // (size-capped, text-only MIME types). session.on("Network.loadingFinished", (event: Protocol.Network.LoadingFinishedEvent) => { const meta = requestMeta.get(event.requestId); requestMeta.delete(event.requestId); if (!logBodies) return; if (event.encodedDataLength > MAX_BODY_SIZE) return; if (!isTextMime(meta?.mimeType)) return; session .send("Network.getResponseBody", { requestId: event.requestId }) .then((result) => { if (result?.body) { logger.record({ type: BrowserEventType.ResponseBody, timestamp: new Date().toISOString(), pageId, targetType, responseBody: { requestId: event.requestId, body: result.body, base64Encoded: result.base64Encoded, }, }); } }) .catch(() => { // Response body not available (redirects, evicted, etc.) — ignore }); }); session.on("Network.loadingFailed", (event: Protocol.Network.LoadingFailedEvent) => { const url = requestMeta.get(event.requestId)?.url; requestMeta.delete(event.requestId); logger.record({ type: BrowserEventType.RequestFailed, timestamp: new Date().toISOString(), pageId, targetType, error: { message: event.errorText, url }, }); }); session.on("Runtime.consoleAPICalled", (event: Protocol.Runtime.ConsoleAPICalledEvent) => { const text = event.args.map(serializeRemoteObject).join(" "); const loc = formatLocation(event.stackTrace); const prefix = targetType === "background_page" ? "[BG] " : ""; logger.record({ type: BrowserEventType.Console, timestamp: new Date().toISOString(), pageId, targetType, console: { level: event.type, text: prefix + text, loc }, }); }); session.on("Runtime.exceptionThrown", (event: Protocol.Runtime.ExceptionThrownEvent) => { const desc = event.exceptionDetails.exception?.description ?? event.exceptionDetails.text; logger.record({ type: BrowserEventType.PageError, timestamp: new Date().toISOString(), pageId, targetType, error: { message: desc }, }); }); page.on("error", (err) => { logger.record({ type: BrowserEventType.Error, timestamp: new Date().toISOString(), pageId, targetType, error: { message: err?.message, stack: err?.stack }, }); }); page.on("pageerror", (err) => { logger.record({ type: BrowserEventType.PageError, timestamp: new Date().toISOString(), pageId, targetType, error: { message: err?.message, stack: err?.stack }, }); }); } ================================================ FILE: api/src/services/cdp/instrumentation/storage/duckdb-storage.ts ================================================ import { Database } from "duckdb-async"; import path from "path"; import fs from "fs/promises"; import { LogStorage, LogQuery, LogQueryResult } from "./log-storage.interface.js"; import { BrowserEventUnion } from "../types.js"; import { randomUUID } from "crypto"; import { safeStringify } from "./safe-json.js"; export type ParquetCompression = "zstd" | "snappy" | "gzip" | "none"; export interface DuckDBStorageOptions { /** * Path to the database file. If not provided, uses in-memory database. */ dbPath?: string; /** * Maximum number of threads DuckDB can use. Defaults to 2. * Set to 1 for minimal CPU impact. */ maxThreads?: number; /** * Memory limit for DuckDB (e.g., "256MB", "1GB"). Defaults to "256MB". */ memoryLimit?: string; /** * Parquet compression algorithm. Defaults to "snappy" (fast, lower CPU). * - "snappy": Fast compression, moderate size (recommended for CPU efficiency) * - "zstd": Best compression ratio, higher CPU * - "gzip": Good compression, moderate CPU * - "none": No compression, fastest but largest files */ parquetCompression?: ParquetCompression; /** * Enable automatic write buffering. When enabled, writes are batched * and flushed periodically to reduce CPU spikes. Defaults to true. */ enableWriteBuffer?: boolean; /** * Size of write buffer before auto-flush. Defaults to 100 events. */ writeBufferSize?: number; /** * Interval in ms to flush write buffer. Defaults to 1000ms. */ writeBufferFlushInterval?: number; } export class DuckDBStorage implements LogStorage { private db: Database | null = null; private dbPath: string; private maxThreads: number; private memoryLimit: string; private parquetCompression: ParquetCompression; private isInitialized = false; // Write buffer for batching private writeBuffer: Array<{ event: BrowserEventUnion; context: Record }> = []; private writeBufferEnabled: boolean; private writeBufferSize: number; private writeBufferFlushInterval: number; private flushTimer: NodeJS.Timeout | null = null; private isFlushing = false; constructor(options: DuckDBStorageOptions = {}) { this.dbPath = options.dbPath || ":memory:"; this.maxThreads = options.maxThreads ?? 2; this.memoryLimit = options.memoryLimit ?? "256MB"; this.parquetCompression = options.parquetCompression ?? "snappy"; this.writeBufferEnabled = options.enableWriteBuffer ?? true; this.writeBufferSize = options.writeBufferSize ?? 100; this.writeBufferFlushInterval = options.writeBufferFlushInterval ?? 1000; } async initialize(): Promise { if (this.isInitialized) return; this.db = await Database.create(this.dbPath, {}); // Throttle CPU usage: limit threads and memory await this.db.run(`SET threads = ${this.maxThreads}`); await this.db.run(`SET memory_limit = '${this.memoryLimit}'`); console.log(`DuckDB initialized: threads=${this.maxThreads}, memory=${this.memoryLimit}`); await this.db.run(` CREATE TABLE IF NOT EXISTS browser_events ( id VARCHAR PRIMARY KEY, timestamp TIMESTAMP NOT NULL, event_type VARCHAR NOT NULL, target_type VARCHAR, page_id VARCHAR, data JSON NOT NULL, context JSON NOT NULL, indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); console.log("Created browser_events table"); await this.db.run(` CREATE INDEX IF NOT EXISTS idx_timestamp ON browser_events(timestamp); `); await this.db.run(` CREATE INDEX IF NOT EXISTS idx_event_type ON browser_events(event_type); `); await this.db.run(` CREATE INDEX IF NOT EXISTS idx_page_id ON browser_events(page_id); `); // Start periodic flush timer if write buffering is enabled if (this.writeBufferEnabled) { this.startFlushTimer(); } this.isInitialized = true; } private startFlushTimer(): void { if (this.flushTimer) return; this.flushTimer = setInterval(() => { this.flushWriteBuffer().catch((err) => { console.error("DuckDB flush error:", err); }); }, this.writeBufferFlushInterval); // Don't block process exit this.flushTimer.unref(); } private stopFlushTimer(): void { if (this.flushTimer) { clearInterval(this.flushTimer); this.flushTimer = null; } } private async flushWriteBuffer(): Promise { if (this.isFlushing || this.writeBuffer.length === 0 || !this.db) return; this.isFlushing = true; const toFlush = this.writeBuffer.splice(0, this.writeBuffer.length); try { await this.writeBatchInternal(toFlush); } catch (err) { // Put events back on failure (at the front) this.writeBuffer.unshift(...toFlush); throw err; } finally { this.isFlushing = false; } } async write(event: BrowserEventUnion, context: Record): Promise { if (!this.db) { throw new Error("Database not initialized"); } if (this.writeBufferEnabled) { // Add to buffer this.writeBuffer.push({ event, context }); // Flush if buffer is full if (this.writeBuffer.length >= this.writeBufferSize) { await this.flushWriteBuffer(); } return; } // Direct write (no buffering) await this.writeSingle(event, context); } private async writeSingle(event: BrowserEventUnion, context: Record): Promise { if (!this.db) return; const stmt = await this.db.prepare(` INSERT INTO browser_events (id, timestamp, event_type, target_type, page_id, data, context, indexed_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `); const id = randomUUID(); const timestamp = event.timestamp; const eventType = event.type; const targetType = event.targetType || null; const pageId = event.pageId || null; const data = safeStringify(event); const contextJson = safeStringify(context); await stmt.run(id, timestamp, eventType, targetType, pageId, data, contextJson); await stmt.finalize(); } async writeBatch( events: Array<{ event: BrowserEventUnion; context: Record }>, ): Promise { if (!this.db) { throw new Error("Database not initialized"); } if (events.length === 0) return; if (this.writeBufferEnabled) { // Add all to buffer this.writeBuffer.push(...events); // Flush if buffer exceeds threshold if (this.writeBuffer.length >= this.writeBufferSize) { await this.flushWriteBuffer(); } return; } // Direct batch write (no buffering) await this.writeBatchInternal(events); } private async writeBatchInternal( events: Array<{ event: BrowserEventUnion; context: Record }>, ): Promise { if (!this.db || events.length === 0) return; const values: string[] = []; const params: any[] = []; for (const { event, context } of events) { values.push("(?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)"); const id = randomUUID(); const timestamp = event.timestamp; const eventType = event.type; const targetType = event.targetType || null; const pageId = event.pageId || null; const data = safeStringify(event); const contextJson = safeStringify(context); params.push(id, timestamp, eventType, targetType, pageId, data, contextJson); } const sql = ` INSERT INTO browser_events (id, timestamp, event_type, target_type, page_id, data, context, indexed_at) VALUES ${values.join(", ")} `; await this.db.run(sql, ...params); } async flush(): Promise { // Flush any buffered writes await this.flushWriteBuffer(); } async query(query: LogQuery): Promise { if (!this.db) { throw new Error("Database not initialized"); } const conditions: string[] = []; const params: any[] = []; if (query.startTime) { conditions.push("timestamp >= ?"); params.push(query.startTime.toISOString()); } if (query.endTime) { conditions.push("timestamp <= ?"); params.push(query.endTime.toISOString()); } if (query.eventTypes && query.eventTypes.length > 0) { conditions.push(`event_type IN (${query.eventTypes.map(() => "?").join(", ")})`); params.push(...query.eventTypes); } if (query.pageId) { conditions.push("page_id = ?"); params.push(query.pageId); } if (query.targetType) { conditions.push("target_type = ?"); params.push(query.targetType); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const limit = query.limit || 100; const offset = query.offset || 0; const countQuery = `SELECT COUNT(*) as total FROM browser_events ${whereClause}`; const countResult = await this.db.all(countQuery, ...params); // COUNT(*) may come back as a BigInt from DuckDB bindings – coerce safely for JSON const total = Number(countResult[0]?.total ?? 0); const eventsQuery = ` SELECT data, context FROM browser_events ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ? `; const rows = await this.db.all(eventsQuery, ...params, limit, offset); const events: BrowserEventUnion[] = rows.map((row: any) => { const data = JSON.parse(row.data); const context = JSON.parse(row.context); return { ...data, ...context }; }); return { events, total, hasMore: offset + limit < total, }; } supportsParquetExport(): boolean { return true; } async exportToParquet(filePath: string, query?: LogQuery): Promise { if (!this.db) { throw new Error("Database not initialized"); } // Build WHERE clause if query is provided let whereClause = ""; const params: any[] = []; if (query) { const conditions: string[] = []; if (query.startTime) { conditions.push("timestamp >= ?"); params.push(query.startTime.toISOString()); } if (query.endTime) { conditions.push("timestamp <= ?"); params.push(query.endTime.toISOString()); } if (query.eventTypes && query.eventTypes.length > 0) { conditions.push(`event_type IN (${query.eventTypes.map(() => "?").join(", ")})`); params.push(...query.eventTypes); } if (query.pageId) { conditions.push("page_id = ?"); params.push(query.pageId); } if (query.targetType) { conditions.push("target_type = ?"); params.push(query.targetType); } whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; } // Ensure output directory exists const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); // Export to Parquet with configurable compression const sanitizedPath = filePath.replace(/'/g, "''"); const compressionClause = this.parquetCompression === "none" ? "" : `, COMPRESSION ${this.parquetCompression.toUpperCase()}`; const exportQuery = ` COPY ( SELECT * FROM browser_events ${whereClause} ) TO '${sanitizedPath}' (FORMAT PARQUET${compressionClause}) `; await this.db.run(exportQuery); return filePath; } async getStats(): Promise<{ totalEvents: number; oldestEvent: Date | null; newestEvent: Date | null; sizeBytes: number; }> { if (!this.db) { throw new Error("Database not initialized"); } const result = await this.db.all(` SELECT COUNT(*) as total, MIN(timestamp) as oldest, MAX(timestamp) as newest FROM browser_events `); const row = result[0]; let sizeBytes = 0; // Get file size if using file-based storage if (this.dbPath !== ":memory:") { const stats = await fs.stat(this.dbPath); sizeBytes = stats.size; } return { totalEvents: Number(row?.total ?? 0), oldestEvent: row?.oldest ? new Date(row.oldest) : null, newestEvent: row?.newest ? new Date(row.newest) : null, sizeBytes, }; } async clear(options: { vacuum?: boolean } = {}): Promise { if (!this.db) { throw new Error("Database not initialized"); } // Flush any pending writes first await this.flushWriteBuffer(); await this.db.run("DELETE FROM browser_events"); // VACUUM is CPU-intensive; run in background by default if (options.vacuum) { await this.db.run("VACUUM"); } else { // Fire-and-forget vacuum (don't block) this.db.run("VACUUM").catch((err) => { console.error("Background VACUUM error:", err); }); } } /** * Run VACUUM manually when CPU is idle. This reclaims disk space * and optimizes the database, but is CPU-intensive. */ async vacuum(): Promise { if (!this.db) { throw new Error("Database not initialized"); } await this.db.run("VACUUM"); } async close(): Promise { // Stop the flush timer this.stopFlushTimer(); // Flush any remaining buffered writes if (this.writeBuffer.length > 0 && this.db) { try { await this.flushWriteBuffer(); } catch (err) { console.error("Error flushing buffer on close:", err); } } if (this.db) { await this.db.close(); this.db = null; } this.isInitialized = false; } /** * Get current buffer stats for monitoring */ getBufferStats(): { bufferedEvents: number; isBufferingEnabled: boolean } { return { bufferedEvents: this.writeBuffer.length, isBufferingEnabled: this.writeBufferEnabled, }; } } ================================================ FILE: api/src/services/cdp/instrumentation/storage/in-memory-storage.ts ================================================ import { LogStorage, LogQuery, LogQueryResult } from "./log-storage.interface.js"; import { BrowserEventUnion } from "../types.js"; interface StoredEvent { event: BrowserEventUnion; context: Record; timestamp: Date; } /** * Simple in-memory log storage for testing or when persistence is not needed. * Note: This implementation does not support Parquet export. */ export class InMemoryStorage implements LogStorage { private events: StoredEvent[] = []; private maxEvents: number; constructor(maxEvents: number = 10000) { this.maxEvents = maxEvents; } async initialize(): Promise { // No initialization needed } async write(event: BrowserEventUnion, context: Record): Promise { this.events.push({ event, context, timestamp: new Date(event.timestamp), }); // Trim old events if we exceed the max if (this.events.length > this.maxEvents) { this.events = this.events.slice(-this.maxEvents); } } async writeBatch( events: Array<{ event: BrowserEventUnion; context: Record }>, ): Promise { for (const { event, context } of events) { await this.write(event, context); } } async query(query: LogQuery): Promise { let filtered = [...this.events]; // Apply filters if (query.startTime) { filtered = filtered.filter((e) => e.timestamp >= query.startTime!); } if (query.endTime) { filtered = filtered.filter((e) => e.timestamp <= query.endTime!); } if (query.eventTypes && query.eventTypes.length > 0) { filtered = filtered.filter((e) => query.eventTypes!.includes(e.event.type)); } if (query.pageId) { filtered = filtered.filter((e) => e.event.pageId === query.pageId); } if (query.targetType) { filtered = filtered.filter((e) => e.event.targetType === query.targetType); } // Sort by timestamp descending filtered.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); const total = filtered.length; const limit = query.limit || 100; const offset = query.offset || 0; const events: BrowserEventUnion[] = filtered .slice(offset, offset + limit) .map((e) => ({ ...e.event, ...e.context })); return { events, total, hasMore: offset + limit < total, }; } supportsParquetExport(): boolean { return false; } async exportToParquet(filePath: string, query?: LogQuery): Promise { throw new Error("Parquet export not supported in InMemoryStorage. Use DuckDBStorage instead."); } async getStats(): Promise<{ totalEvents: number; oldestEvent: Date | null; newestEvent: Date | null; sizeBytes: number; }> { const timestamps = this.events.map((e) => e.timestamp.getTime()); return { totalEvents: this.events.length, oldestEvent: timestamps.length > 0 ? new Date(Math.min(...timestamps)) : null, newestEvent: timestamps.length > 0 ? new Date(Math.max(...timestamps)) : null, sizeBytes: JSON.stringify(this.events).length, // Approximate }; } async clear(): Promise { this.events = []; } async flush(): Promise { // No-op for in-memory storage } async close(): Promise { this.events = []; } } ================================================ FILE: api/src/services/cdp/instrumentation/storage/index.ts ================================================ export * from "./log-storage.interface.js"; export * from "./duckdb-storage.js"; export * from "./in-memory-storage.js"; ================================================ FILE: api/src/services/cdp/instrumentation/storage/log-storage.interface.ts ================================================ import { BrowserEventUnion } from "../types.js"; export interface LogQuery { startTime?: Date; endTime?: Date; eventTypes?: string[]; pageId?: string; targetType?: string; limit?: number; offset?: number; } export interface LogQueryResult { events: BrowserEventUnion[]; total: number; hasMore: boolean; } export interface LogStorage { /** * Initialize the storage backend */ initialize(): Promise; /** * Write a single event to storage */ write(event: BrowserEventUnion, context: Record): Promise; /** * Write multiple events in batch */ writeBatch( events: Array<{ event: BrowserEventUnion; context: Record }>, ): Promise; /** * Query events from storage */ query(query: LogQuery): Promise; /** * Check if Parquet export is supported by this storage backend */ supportsParquetExport(): boolean; /** * Export logs to Parquet format */ exportToParquet(filePath: string, query?: LogQuery): Promise; /** * Get statistics about stored logs */ getStats(): Promise<{ totalEvents: number; oldestEvent: Date | null; newestEvent: Date | null; sizeBytes: number; }>; /** * Clear all logs */ clear(): Promise; /** * Flush any pending writes */ flush(): Promise; /** * Close the storage connection */ close(): Promise; } ================================================ FILE: api/src/services/cdp/instrumentation/storage/safe-json.ts ================================================ export function safeStringify(value: unknown): string { const seen = new WeakSet(); return JSON.stringify(value, function (_key, currentValue) { if (typeof currentValue === "object" && currentValue !== null) { if (seen.has(currentValue)) { return "[Circular]"; } seen.add(currentValue); } return currentValue; }); } ================================================ FILE: api/src/services/cdp/instrumentation/target-manager.ts ================================================ import { type Target, type CDPSession, TargetType } from "puppeteer-core"; import type { FastifyBaseLogger } from "fastify"; import { attachPageEvents, AttachPageEventsOptions } from "./page-events.js"; import { attachCDPEvents } from "./cdp-events.js"; import { attachExtensionEvents } from "./extension-events.js"; import { attachWorkerEvents } from "./worker-events.js"; import { BrowserLogger } from "./browser-logger.js"; const INTERNAL_EXTENSIONS = new Set([ // TODO: need secret manager, recorder, and capacha IDs ]); export class TargetInstrumentationManager { private attachedSessions = new Set(); private cdpSessions = new Map(); private pageEventsOptions: AttachPageEventsOptions; constructor( private logger: BrowserLogger, private appLogger: FastifyBaseLogger, pageEventsOptions?: AttachPageEventsOptions, ) { this.pageEventsOptions = pageEventsOptions ?? {}; } async attach(target: Target, type: TargetType) { const url = target.url?.() ?? ""; const isExtensionTarget = url.startsWith("chrome-extension://"); const sessionId = (target as any)._targetId; if (this.attachedSessions.has(sessionId)) { return; } this.attachedSessions.add(sessionId); switch (type) { case TargetType.PAGE: case TargetType.BACKGROUND_PAGE: { // Create a single CDP session shared by page-events and cdp-events const session = await target.createCDPSession(); this.cdpSessions.set(sessionId, session); await this.enableDomainsForTarget(session, type, isExtensionTarget); const page = await target.page(); if (page) { await attachPageEvents(page, session, this.logger, type, this.pageEventsOptions); } attachCDPEvents(session, this.logger); if (isExtensionTarget) { await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger); } break; } case TargetType.SERVICE_WORKER: { const session = await target.createCDPSession(); this.cdpSessions.set(sessionId, session); await this.enableDomainsForTarget(session, type, isExtensionTarget); attachCDPEvents(session, this.logger); if (isExtensionTarget) { await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger); } else { attachWorkerEvents(target, session, this.logger, type); } break; } case TargetType.SHARED_WORKER: { const session = await target.createCDPSession(); this.cdpSessions.set(sessionId, session); await this.enableDomainsForTarget(session, type, isExtensionTarget); attachCDPEvents(session, this.logger); if (isExtensionTarget) { await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger); } else { attachWorkerEvents(target, session, this.logger, type); } break; } case TargetType.WEBVIEW: { const session = await target.createCDPSession(); this.cdpSessions.set(sessionId, session); await this.enableDomainsForTarget(session, type, isExtensionTarget); attachCDPEvents(session, this.logger); if (isExtensionTarget) { await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger); } else { attachWorkerEvents(target, session, this.logger, type); } break; } case TargetType.BROWSER: case TargetType.OTHER: default: { const session = await target.createCDPSession(); this.cdpSessions.set(sessionId, session); await this.enableDomainsForTarget(session, type, isExtensionTarget); attachCDPEvents(session, this.logger); if (isExtensionTarget) { await attachExtensionEvents(target, this.logger, INTERNAL_EXTENSIONS, this.appLogger); } break; } } } detach(targetId: string) { this.attachedSessions.delete(targetId); const session = this.cdpSessions.get(targetId); if (session) { this.cdpSessions.delete(targetId); session.detach().catch(() => { // Session may already be closed if the target was destroyed }); } } private async enableDomainsForTarget( session: CDPSession, type: TargetType, isExtension: boolean, ): Promise { const enabledDomains = new Set(); const enable = async (domain: string) => { if (enabledDomains.has(domain)) return; try { await session.send(`${domain}.enable` as any); enabledDomains.add(domain); } catch (err) { this.appLogger.error({ err }, `[TargetManager] Failed to enable ${domain} for ${type}:`); } }; switch (type) { case TargetType.PAGE: case TargetType.BACKGROUND_PAGE: await enable("Runtime"); await enable("Log"); await enable("Network"); break; case TargetType.SERVICE_WORKER: case TargetType.SHARED_WORKER: await enable("Runtime"); await enable("Log"); if (isExtension) { await enable("Network"); } break; case TargetType.WEBVIEW: case TargetType.OTHER: if (isExtension) { await enable("Runtime"); await enable("Log"); await enable("Network"); } break; default: break; } } } ================================================ FILE: api/src/services/cdp/instrumentation/types.ts ================================================ import type { TargetType } from "puppeteer-core"; import type { BrowserEventType } from "../../../types/enums.js"; export interface BaseBrowserEvent { type: BrowserEventType; timestamp: string; targetType?: TargetType; pageId?: string; } export interface RequestEvent extends BaseBrowserEvent { type: BrowserEventType.Request; request: { method: string; url: string; resourceType?: string; postData?: string; headers?: Record; }; } export interface ResponseEvent extends BaseBrowserEvent { type: BrowserEventType.Response; response: { status: number; url: string; mimeType?: string; headers?: Record; body?: string; }; } export interface ResponseBodyEvent extends BaseBrowserEvent { type: BrowserEventType.ResponseBody; responseBody: { requestId: string; body: string; base64Encoded: boolean; }; } export interface NavigationEvent extends BaseBrowserEvent { type: BrowserEventType.Navigation; navigation: { url: string }; } export interface ConsoleEvent extends BaseBrowserEvent { type: BrowserEventType.Console; console: { level: string; text: string; loc?: string }; } export interface ErrorEvent extends BaseBrowserEvent { type: | BrowserEventType.PageError | BrowserEventType.BrowserError | BrowserEventType.Error | BrowserEventType.RequestFailed; error: { message: string; stack?: string; url?: string }; } export interface RecordingEvent extends BaseBrowserEvent { type: BrowserEventType.Recording | BrowserEventType.ScreencastFrame; data: any; } export interface CDPEvent extends BaseBrowserEvent { type: BrowserEventType.CDPEvent; cdp: { name: string; params?: object; }; } export interface CDPCommandEvent extends BaseBrowserEvent { type: BrowserEventType.CDPCommand; cdp: { command: string; params?: object; sessionId: string; }; } export interface CDPCommandResultEvent extends BaseBrowserEvent { type: BrowserEventType.CDPCommandResult; cdp: { command: string; duration: number; sessionId: string; success: boolean; error?: string; }; } export interface ExtensionEvent extends BaseBrowserEvent { type: BrowserEventType.Console | BrowserEventType.PageError | BrowserEventType.RequestFailed; extensionId: string; serviceWorkerId?: string; logLevel: "log" | "warn" | "error"; message: string; loc?: string; executionContextId?: number; } export type BrowserEventUnion = | RequestEvent | ResponseEvent | ResponseBodyEvent | NavigationEvent | ConsoleEvent | ErrorEvent | RecordingEvent | CDPEvent | CDPCommandEvent | CDPCommandResultEvent | ExtensionEvent; ================================================ FILE: api/src/services/cdp/instrumentation/utils.ts ================================================ import type { Target, Protocol } from "puppeteer-core"; import { safeStringify } from "./storage/safe-json.js"; export function extractTargetId(target: Target): string { return (target as any)._targetId as string; } export function serializeRemoteObject(obj: Protocol.Runtime.RemoteObject): string { if (obj.value !== undefined) { return typeof obj.value === "object" ? safeStringify(obj.value) : String(obj.value); } return obj.description ?? ""; } export function formatLocation(stackTrace?: Protocol.Runtime.StackTrace): string | undefined { if (!stackTrace?.callFrames?.[0]) return undefined; const frame = stackTrace.callFrames[0]; return `${frame.url}:${frame.lineNumber + 1}:${frame.columnNumber + 1}`; } ================================================ FILE: api/src/services/cdp/instrumentation/worker-events.ts ================================================ import type { Target, Protocol, TargetType, CDPSession } from "puppeteer-core"; import { BrowserEventType } from "../../../types/index.js"; import { BrowserLogger } from "./browser-logger.js"; import { extractTargetId, formatLocation, serializeRemoteObject } from "./utils.js"; export function attachWorkerEvents( target: Target, session: CDPSession, logger: BrowserLogger, targetType: TargetType, ): void { const targetId = extractTargetId(target); session.on("Runtime.consoleAPICalled", (event: Protocol.Runtime.ConsoleAPICalledEvent) => { const text = event.args.map(serializeRemoteObject).join(" "); const loc = formatLocation(event.stackTrace); logger.record({ type: BrowserEventType.Console, timestamp: new Date().toISOString(), pageId: targetId, targetType, console: { level: event.type, text, loc }, }); }); session.on("Runtime.exceptionThrown", (event: Protocol.Runtime.ExceptionThrownEvent) => { const desc = event.exceptionDetails.exception?.description ?? event.exceptionDetails.text; logger.record({ type: BrowserEventType.PageError, timestamp: new Date().toISOString(), pageId: targetId, targetType, error: { message: desc }, }); }); } ================================================ FILE: api/src/services/cdp/plugins/core/base-plugin.ts ================================================ import type { Browser, Page } from "puppeteer-core"; import type { CDPService } from "../../cdp.service.js"; import type { BrowserLauncherOptions } from "../../../../types/browser.js"; export interface PluginOptions { name: string; [key: string]: any; } export abstract class BasePlugin { public name: string; protected options: PluginOptions; protected cdpService: CDPService | null; constructor(options: PluginOptions) { this.name = options.name; this.options = options; this.cdpService = null; } public setService(service: CDPService): void { this.cdpService = service; } // Lifecycle methods public async onBrowserLaunch(browser: Browser): Promise {} public onBrowserReady(context: BrowserLauncherOptions): void | Promise {} public async onPageCreated(page: Page): Promise {} public async onPageNavigate(page: Page): Promise {} public async onPageUnload(page: Page): Promise {} public async onBrowserClose(browser: Browser): Promise {} public async onBeforePageClose(page: Page): Promise {} public async onShutdown(): Promise {} public async onSessionEnd(sessionConfig: BrowserLauncherOptions): Promise {} } export type { BrowserLauncherOptions }; ================================================ FILE: api/src/services/cdp/plugins/core/index.ts ================================================ export * from "./base-plugin.js"; export * from "./plugin-manager.js"; ================================================ FILE: api/src/services/cdp/plugins/core/plugin-manager.ts ================================================ import { Browser, Page } from "puppeteer-core"; import { CDPService } from "../../cdp.service.js"; import { BasePlugin } from "./base-plugin.js"; import { FastifyBaseLogger } from "fastify"; import { BrowserLauncherOptions } from "../../../../types/browser.js"; export class PluginManager { private plugins: Map; private service: CDPService; private logger: FastifyBaseLogger; constructor(service: CDPService, logger: FastifyBaseLogger) { this.plugins = new Map(); this.service = service; this.logger = logger; } /** * Register a plugin with the plugin manager */ public register(plugin: BasePlugin): void { if (this.plugins.has(plugin.name)) { this.logger.warn(`Plugin with name ${plugin.name} is already registered. Overwriting.`); } plugin.setService(this.service); this.plugins.set(plugin.name, plugin); this.logger.info(`Registered plugin: ${plugin.name}`); } /** * Unregister a plugin from the plugin manager */ public unregister(pluginName: string): boolean { const result = this.plugins.delete(pluginName); if (result) { this.logger.info(`Unregistered plugin: ${pluginName}`); } else { this.logger.warn(`Plugin with name ${pluginName} was not registered`); } return result; } /** * Get a plugin by name */ public getPlugin(pluginName: string): T | undefined { return this.plugins.get(pluginName) as T | undefined; } /** * Notify all plugins about a browser launch */ public async onBrowserLaunch(browser: Browser): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { await plugin.onBrowserLaunch(browser); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onBrowserLaunch: ${error}`); } }); await Promise.all(promises); } public async onBrowserReady(context: BrowserLauncherOptions): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { // handle both async and sync hooks await Promise.resolve(plugin.onBrowserReady(context)); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onBrowserReady: ${error}`); } }); await Promise.all(promises); } /** * Notify all plugins about a page creation */ public async onPageCreated(page: Page): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { await plugin.onPageCreated(page); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onPageCreated: ${error}`); } }); await Promise.all(promises); } /** * Notify all plugins before browser closes */ public async onBrowserClose(browser: Browser): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { await plugin.onBrowserClose(browser); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onBrowserClose: ${error}`); } }); await Promise.all(promises); } /** * Notify all plugins before a page navigates */ public async onPageNavigate(page: Page): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { await plugin.onPageNavigate(page); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onPageNavigate: ${error}`); } }); await Promise.all(promises); } /** * Notify all plugins before a page unloads */ public async onPageUnload(page: Page): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { await plugin.onPageUnload(page); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onPageUnload: ${error}`); } }); await Promise.all(promises); } /** * Notify all plugins before a page closes */ public async onBeforePageClose(page: Page): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { await plugin.onBeforePageClose(page); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onBeforePageClose: ${error}`); } }); await Promise.all(promises); } /** * Notify all plugins about shutdown */ public async onShutdown(): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { await plugin.onShutdown(); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onShutdown: ${error}`); } }); await Promise.all(promises); } /** * Notify all plugins when a session has ended */ public async onSessionEnd(sessionConfig: BrowserLauncherOptions): Promise { const promises = Array.from(this.plugins.values()).map(async (plugin) => { try { await plugin.onSessionEnd(sessionConfig); } catch (error) { this.logger.error(`Error in plugin ${plugin.name}.onSessionEnd: ${error}`); } }); await Promise.all(promises); } } ================================================ FILE: api/src/services/cdp/plugins/pptr-extensions.d.ts ================================================ import { SessionManager } from "./session/session-manager.js"; declare module "puppeteer-core" { interface Page { session: SessionManager; } } ================================================ FILE: api/src/services/cdp/utils/error-handlers.ts ================================================ import { FastifyBaseLogger } from "fastify"; import { BaseLaunchError } from "../errors/launch-errors.js"; /** * Executes a critical operation that must succeed. Throws a categorized error on failure. * * @param operation - The async operation to execute * @param errorFactory - Factory function to create a categorized error from the caught error * @returns The result of the operation * @throws {BaseLaunchError} When the operation fails * * @example * const result = await executeCritical( * async () => doSomethingCritical(), * (error) => new BrowserProcessError(String(error), BrowserProcessState.LAUNCH_FAILED, error) * ); */ export async function executeCritical( operation: () => Promise, errorFactory: (error: unknown) => BaseLaunchError, ): Promise { try { return await operation(); } catch (error) { throw errorFactory(error); } } /** * Executes a non-critical operation. Logs a warning on failure but continues execution. * * @param logger - Fastify logger instance for warning messages * @param operation - The async operation to execute * @param errorFactory - Factory function to create a categorized error from the caught error * @param defaultValue - Optional default value to return on failure * @returns The result of the operation, or defaultValue/undefined on failure * * @example * const result = await executeOptional( * logger, * async () => tryOptionalOperation(), * (error) => new CleanupError(String(error), CleanupType.PRE_LAUNCH_FILE_CLEANUP), * defaultValue * ); */ export async function executeOptional( logger: FastifyBaseLogger, operation: () => Promise, errorFactory: (error: unknown) => BaseLaunchError, defaultValue?: T, ): Promise { try { return await operation(); } catch (error) { const launchError = errorFactory(error); logger.warn(`[CDPService] ${launchError.message} - continuing with launch`); return defaultValue; } } /** * Executes a best-effort operation. Silently logs on failure. * * @param logger - Fastify logger instance for debug messages * @param operation - The async operation to execute * @param logMessage - Message to log on failure * @returns The result of the operation, or undefined on failure * * @example * const result = await executeBestEffort( * logger, * async () => tryBestEffortOperation(), * "Failed to configure optional feature" * ); */ export async function executeBestEffort( logger: FastifyBaseLogger, operation: () => Promise, logMessage: string, ): Promise { try { return await operation(); } catch (error) { logger.debug(`[CDPService] ${logMessage}: ${error}`); return undefined; } } ================================================ FILE: api/src/services/cdp/utils/validation.ts ================================================ import { FastifyBaseLogger } from "fastify"; import { BrowserLauncherOptions } from "../../../types/index.js"; import { ConfigurationError, ConfigurationField } from "../errors/launch-errors.js"; /** * Compares two Promise values by resolving them and checking if their serialized * representations are equal. * @param current - Current value or Promise * @param next - Next value or Promise * @returns Promise - True if serialized values are equal */ export async function comparePromiseValues( current: T | Promise, next: T | Promise, ): Promise { try { const [currentValue, nextValue] = await Promise.all([ Promise.resolve(current), Promise.resolve(next), ]); return JSON.stringify(currentValue) === JSON.stringify(nextValue); } catch (error) { // If either promise rejects, consider them not equal return false; } } /** * Validates a given launch configuration (not conclusive) */ export function validateLaunchConfig(config: BrowserLauncherOptions): void { // Validate dimensions if (config.dimensions) { if (config.dimensions.width <= 0 || config.dimensions.height <= 0) { throw new ConfigurationError( "Dimensions must be positive numbers", ConfigurationField.DIMENSIONS, config.dimensions, ); } if (config.dimensions.width > 7680 || config.dimensions.height > 4320) { throw new ConfigurationError( "Dimensions are unreasonably large (max 7680x4320)", ConfigurationField.DIMENSIONS, config.dimensions, ); } } // Validates proxy URL format if (config.options.proxyUrl) { try { new URL(config.options.proxyUrl); } catch { throw new ConfigurationError( `Invalid proxy URL format: ${config.options.proxyUrl}`, ConfigurationField.PROXY_URL, config.options.proxyUrl, ); } } } /** * Validates and resolves the timezone configuration * @param logger - Fastify logger instance for warning messages * @param timezonePromise - Promise resolving to the timezone string * @param timeoutMs - Maximum time to wait for timezone resolution (default: 10000ms) * @returns Resolved and validated timezone string * @throws ConfigurationError if the timezone is invalid or cannot be resolved */ export async function validateTimezone( logger: FastifyBaseLogger, timezonePromise: Promise, timeoutMs: number = 10000, ): Promise { try { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Timezone validation timeout after ${timeoutMs}ms`)); }, timeoutMs); }); const timezone = await Promise.race([timezonePromise, timeoutPromise]); try { Intl.DateTimeFormat(undefined, { timeZone: timezone }); return timezone; } catch (timezoneError) { throw new ConfigurationError( `Invalid timezone resolved: ${timezone}`, ConfigurationField.TIMEZONE, timezone, ); } } catch (error) { if (error instanceof ConfigurationError) { throw error; } throw new ConfigurationError( `Failed to resolve timezone: ${error}`, ConfigurationField.TIMEZONE, undefined, ); } } /** * Checks if two launch configurations are reusable * @param current - The current launch configuration * @param next - The next launch configuration * @returns True if the configurations are reusable, false otherwise */ export async function isSimilarConfig( current?: BrowserLauncherOptions, next?: BrowserLauncherOptions, ): Promise { if (!current || !next) { return false; } // Start timezone comparison immediately (don't await yet) // This allows the Promise to resolve in parallel with our synchronous checks const timezoneComparisonPromise = comparePromiseValues( current.timezone || Promise.resolve(""), next.timezone || Promise.resolve(""), ); const normalizeArgs = (args?: string[]) => (args || []).filter(Boolean).slice().sort(); const normalizeExt = (ext?: string[]) => (ext || []).slice().sort(); const currentHeadless = current.options?.headless ?? true; const nextHeadless = next.options?.headless ?? true; const currentProxy = current.options?.proxyUrl || ""; const nextProxy = next.options?.proxyUrl || ""; const currentArgs = normalizeArgs(current.options?.args); const nextArgs = normalizeArgs(next.options?.args); const currentExt = normalizeExt(current.extensions); const nextExt = normalizeExt(next.extensions); const currentBlockAds = current.blockAds ?? true; const nextBlockAds = next.blockAds ?? true; const currentUserAgent = current.userAgent || ""; const nextUserAgent = next.userAgent || ""; const currentUserDataDir = current.userDataDir || ""; const nextUserDataDir = next.userDataDir || ""; const currentSkipFingerprint = current.skipFingerprintInjection ?? false; const nextSkipFingerprint = next.skipFingerprintInjection ?? false; const currentWidth = current.dimensions?.width ?? 1920; const nextWidth = next.dimensions?.width ?? 1920; const currentHeight = current.dimensions?.height ?? 1080; const nextHeight = next.dimensions?.height ?? 1080; const { session: _s1, streaming: _w1, ...currentExtra } = (current.extra ?? {}) as Record; const { session: _s2, streaming: _w2, ...nextExtra } = (next.extra ?? {}) as Record; return ( currentHeadless === nextHeadless && currentProxy === nextProxy && currentUserAgent === nextUserAgent && currentUserDataDir === nextUserDataDir && currentSkipFingerprint === nextSkipFingerprint && currentWidth === nextWidth && currentHeight === nextHeight && currentBlockAds === nextBlockAds && JSON.stringify(currentArgs) === JSON.stringify(nextArgs) && JSON.stringify(currentExt) === JSON.stringify(nextExt) && JSON.stringify(currentExtra) === JSON.stringify(nextExtra) && JSON.stringify(current.userPreferences) === JSON.stringify(next.userPreferences) && JSON.stringify(current.deviceConfig) === JSON.stringify(next.deviceConfig) && (await timezoneComparisonPromise) ); } ================================================ FILE: api/src/services/context/chrome-context.service.ts ================================================ import { EventEmitter } from "events"; import { FastifyBaseLogger } from "fastify"; import { getProfilePath } from "../../utils/context.js"; import { ChromeLocalStorageReader } from "../leveldb/localstorage.js"; import { ChromeSessionStorageReader } from "../leveldb/sessionstorage.js"; import { SessionData } from "./types.js"; export class ChromeContextService extends EventEmitter { private logger: FastifyBaseLogger; constructor(logger: FastifyBaseLogger) { super(); this.logger = logger; } /** * Get all session data from a Chrome user data directory * @param userDataDir Path to Chrome user data directory * @returns SessionData containing cookies, localStorage, sessionStorage, and more */ public async getSessionData(userDataDir?: string): Promise { if (!userDataDir) { this.logger.warn("No userDataDir specified, returning empty session data"); return { localStorage: {}, sessionStorage: {}, indexedDB: {}, cookies: [], }; } this.logger.info(`Extracting session data from Chrome user data directory: ${userDataDir}`); try { const sessionData: SessionData = {}; const [localStorage, sessionStorage] = await Promise.all([ this.extractLocalStorage(userDataDir), this.extractSessionStorage(userDataDir), ]); if (localStorage && Object.keys(localStorage).length > 0) { sessionData.localStorage = localStorage; } if (sessionStorage && Object.keys(sessionStorage).length > 0) { sessionData.sessionStorage = sessionStorage; } return sessionData; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Error extracting session data: ${errorMessage}`); throw new Error(`Failed to extract session data: ${errorMessage}`); } } /** * Extract localStorage from Chrome's LevelDB database */ private async extractLocalStorage( userDataDir: string, ): Promise>> { const localStoragePath = getProfilePath(userDataDir, "Local Storage", "leveldb"); this.logger.info(`Extracting localStorage from ${localStoragePath}`); try { this.logger.info(`Reading localStorage from ${localStoragePath}`); return await ChromeLocalStorageReader.readLocalStorage(localStoragePath); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Error extracting localStorage: ${errorMessage}`); return {}; } } /** * Extract sessionStorage from Chrome's Session Storage */ private async extractSessionStorage( userDataDir: string, ): Promise>> { // Normalize path for cross-platform compatibility const sessionStoragePath = getProfilePath(userDataDir, "Session Storage"); try { this.logger.info(`Reading sessionStorage from ${sessionStoragePath}`); const sessionStorage = await ChromeSessionStorageReader.readSessionStorage(sessionStoragePath); return sessionStorage; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Error extracting sessionStorage: ${errorMessage}`); return {}; } } } ================================================ FILE: api/src/services/context/types.ts ================================================ import { z } from "zod"; export enum StorageProviderName { Cookies = "cookies", LocalStorage = "localStorage", SessionStorage = "sessionStorage", IndexedDB = "indexedDB", } export interface IndexedDBDatabaseWithOrigin { id: number; name: string; origin: string; objectStores: IndexedDBObjectStore[]; } export interface IndexedDBDatabase { id: number; name: string; data: IndexedDBObjectStore[]; } export interface IndexedDBObjectStore { id: number; name: string; records: IndexedDBRecord[]; } export interface IndexedDBRecord { key: any; value: any; blobFiles?: IndexedDBBlobFile[]; } export interface IndexedDBBlobFile { blobNumber: number; mimeType: string; size: number; filename?: string; lastModified?: Date; path?: string; } export type LocalStorageData = Record; export type SessionStorageData = Record; export type CookieData = z.infer; export interface StorageProviderDataMap { [StorageProviderName.LocalStorage]: LocalStorageData; [StorageProviderName.SessionStorage]: SessionStorageData; [StorageProviderName.Cookies]: CookieData[]; [StorageProviderName.IndexedDB]: Array; } // Utility type to get the data type for a specific provider export type ProviderDataType = StorageProviderDataMap[T]; export type SessionData = { [StorageProviderName.Cookies]?: CookieData[]; [StorageProviderName.LocalStorage]?: Record; [StorageProviderName.SessionStorage]?: Record; [StorageProviderName.IndexedDB]?: Record>; }; // Error classes export class CorruptedSessionDataError extends Error { constructor(zodError: z.ZodError) { super(`Session data is corrupted: ${zodError.message}`); this.name = "CorruptedSessionDataError"; } } // CDP related schemas export const CDPSameSite = z.enum(["Strict", "Lax", "None"]); export const CDPCookiePriority = z.enum(["Low", "Medium", "High"]); export const CDPSourceScheme = z.enum(["Unset", "NonSecure", "Secure"]); /** * CDP Network.Cookie schema * @see https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie */ export const CDPCookieSchema = z.object({ name: z.string().describe("The name of the cookie"), value: z.string().describe("The value of the cookie"), url: z.string().optional().describe("The URL of the cookie"), domain: z.string().optional().describe("The domain of the cookie"), path: z.string().optional().describe("The path of the cookie"), secure: z.boolean().optional().describe("Whether the cookie is secure"), httpOnly: z.boolean().optional().describe("Whether the cookie is HTTP only"), sameSite: CDPSameSite.optional().describe("The same site attribute of the cookie"), size: z.number().optional().describe("The size of the cookie"), expires: z.number().optional().describe("The expiration date of the cookie"), partitionKey: z .object({ topLevelSite: z .string() .describe( "The site of the top-level URL the browser was visiting at the start of the request to the endpoint that set the cookie.", ), hasCrossSiteAncestor: z .boolean() .describe( "Indicates if the cookie has any ancestors that are cross-site to the topLevelSite.", ), }) .optional() .describe("The partition key of the cookie"), session: z.boolean().optional().describe("Whether the cookie is a session cookie"), priority: CDPCookiePriority.optional().describe("The priority of the cookie"), sameParty: z.boolean().optional().describe("Whether the cookie is a same party cookie"), sourceScheme: CDPSourceScheme.optional().describe("The source scheme of the cookie"), sourcePort: z.number().optional().describe("The source port of the cookie"), }); export type CDPCookie = z.infer; // IndexedDB related schemas export const IndexedDBBlobFileSchema = z.object({ blobNumber: z.number(), mimeType: z.string(), size: z.number(), filename: z.string().optional(), lastModified: z.date().optional(), path: z.string().optional(), }); export const IndexedDBRecordSchema = z.object({ key: z.any(), value: z.any(), blobFiles: z.array(IndexedDBBlobFileSchema).optional(), }); export const IndexedDBObjectStoreSchema = z.object({ id: z.number(), name: z.string(), records: z.array(IndexedDBRecordSchema), }); export const IndexedDBDatabaseSchema = z.object({ id: z.number(), name: z.string(), data: z.array(IndexedDBObjectStoreSchema), }); // Update the existing schema to use the new schemas export const SessionContextSchema = z.object({ [StorageProviderName.Cookies]: z .array(CDPCookieSchema) .optional() .describe("Cookies to initialize in the session"), [StorageProviderName.LocalStorage]: z .record(z.string(), z.record(z.string(), z.string())) .optional() .describe("Domain-specific localStorage items to initialize in the session"), [StorageProviderName.SessionStorage]: z .record(z.string(), z.record(z.string(), z.string())) .optional() .describe("Domain-specific sessionStorage items to initialize in the session"), [StorageProviderName.IndexedDB]: z .record(z.string(), z.array(IndexedDBDatabaseSchema)) .optional() .describe("Domain-specific indexedDB items to initialize in the session"), }); ================================================ FILE: api/src/services/file.service.ts ================================================ import archiver from "archiver"; import chokidar, { FSWatcher } from "chokidar"; import fs from "fs"; import type { DebouncedFunc } from "lodash-es"; import { debounce } from "lodash-es"; import { tmpdir } from "os"; import path, { resolve } from "path"; import { Readable } from "stream"; import { env } from "../env.js"; interface File { size: number; lastModified: Date; } export class FileService { private baseFilesPath: string; private fileWatcher: FSWatcher | null = null; private static instance: FileService | null = null; private prebuiltArchiveDir: string; private prebuiltArchivePath: string; private isArchiving: boolean = false; private archiveDebounceTime = 500; private debouncedCreateArchive: DebouncedFunc<() => Promise>; private constructor() { this.baseFilesPath = env.NODE_ENV === "development" ? path.join(tmpdir(), "files") : "/files"; this.prebuiltArchiveDir = "/tmp/.steel"; this.prebuiltArchivePath = path.join(this.prebuiltArchiveDir, "files.zip"); fs.mkdirSync(this.baseFilesPath, { recursive: true }); const boundCreateArchive = this._createArchive.bind(this); this.debouncedCreateArchive = debounce(boundCreateArchive, this.archiveDebounceTime); this.initFileWatcher(); } public static getInstance() { if (!FileService.instance) { FileService.instance = new FileService(); } return FileService.instance; } private async handleFileAdd(filePath: string) { console.log(`[FileService] File added detected: ${filePath}`); this.debouncedCreateArchive(); } private handleFileDelete(filePath: string) { console.log(`[FileService] File deleted detected: ${filePath}`); this.debouncedCreateArchive(); } private handleDirChange(filePath: string) { console.log(`[FileService] Directory change detected: ${filePath}`); this.debouncedCreateArchive(); } private initFileWatcher() { this.fileWatcher = chokidar.watch(this.baseFilesPath, { ignored: /(^|[\/\\])\../, persistent: true, ignoreInitial: false, awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100, }, depth: undefined, }); console.log(`[FileService] File watcher initialized for ${this.baseFilesPath}`); this.fileWatcher .on("add", (filePath) => this.handleFileAdd(filePath)) .on("unlink", (filePath) => this.handleFileDelete(filePath)) .on("addDir", (filePath) => this.handleDirChange(filePath)) .on("unlinkDir", (filePath) => this.handleDirChange(filePath)) .on("error", (error) => console.error(`Watcher error: ${error}`)) .on("ready", () => { console.log("[FileService] Initial scan complete. Ready for changes."); this.debouncedCreateArchive(); }); } private getSafeFilePath(relativePath: string) { const resolvedPath = resolve(this.baseFilesPath, relativePath); if ( !resolvedPath.startsWith(this.baseFilesPath + path.sep) && resolvedPath !== this.baseFilesPath ) { throw new Error("Invalid path"); } return resolvedPath; } private async exists(filePath: string): Promise { try { await fs.promises.stat(filePath); return true; } catch (err: any) { if (err.code === "ENOENT") return false; throw err; } } public async saveFile({ filePath, stream, }: { filePath: string; stream: Readable; }): Promise { await fs.promises.mkdir(this.baseFilesPath, { recursive: true }); const safeFilePath = this.getSafeFilePath(filePath); const parentDir = path.dirname(safeFilePath); await fs.promises.mkdir(parentDir, { recursive: true }); try { await fs.promises.writeFile(safeFilePath, stream); const stats = await fs.promises.stat(safeFilePath); const file: File = { size: stats.size, lastModified: stats.mtime, }; console.log(`File saved: ${safeFilePath}, Size: ${file.size}`); this.debouncedCreateArchive(); return { ...file, path: safeFilePath }; } catch (error) { console.error(`[FileService] Error saving file ${safeFilePath}:`, error); try { if (await this.exists(safeFilePath)) { await fs.promises.unlink(safeFilePath); } } catch (cleanupErr) { console.error( `[FileService] Failed to cleanup file ${safeFilePath} after save error:`, cleanupErr, ); } throw error; } } public async downloadFile({ filePath, }: { filePath: string; }): Promise<{ stream: Readable } & File> { await fs.promises.mkdir(this.baseFilesPath, { recursive: true }); const safeFilePath = this.getSafeFilePath(filePath); try { const stats = await fs.promises.stat(safeFilePath); if (!stats.isFile()) { throw new Error(`Requested path is not a file: ${safeFilePath}`); } const file: File = { size: stats.size, lastModified: stats.mtime, }; const stream = fs.createReadStream(safeFilePath); return { stream, ...file, }; } catch (error: any) { if (error.code === "ENOENT") { throw new Error(`File not found: ${safeFilePath}`); } console.error(`[FileService] Error accessing file ${safeFilePath} for download:`, error); throw new Error(`File not found or inaccessible: ${safeFilePath}`); } } public async getFile({ filePath }: { filePath: string }): Promise { await fs.promises.mkdir(this.baseFilesPath, { recursive: true }); const safeFilePath = this.getSafeFilePath(filePath); try { const stats = await fs.promises.stat(safeFilePath); if (!stats.isFile()) { throw new Error(`Requested path is not a file: ${safeFilePath}`); } const file: File = { size: stats.size, lastModified: stats.mtime, }; return file; } catch (error: any) { if (error.code === "ENOENT") { throw new Error(`File not found: ${safeFilePath}`); } console.error(`[FileService] Error accessing file ${safeFilePath} for getFile:`, error); throw new Error(`File not found or inaccessible: ${safeFilePath}`); } } public async listFiles(): Promise> { await fs.promises.mkdir(this.baseFilesPath, { recursive: true }); const allFiles: Array<{ path: string } & File> = []; const collectFilesRecursively = async (currentDir: string) => { try { const entries = await fs.promises.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const entryPath = path.join(currentDir, entry.name); if (entry.isFile()) { try { const stats = await fs.promises.stat(entryPath); allFiles.push({ path: entryPath, size: stats.size, lastModified: stats.mtime, }); } catch (statError) { console.error( `[FileService] Error getting stats for file ${entryPath} during listFiles:`, statError, ); } } else if (entry.isDirectory()) { await collectFilesRecursively(entryPath); } } } catch (readDirError) { console.error( `[FileService] Error reading directory ${currentDir} during listFiles:`, readDirError, ); } }; try { await collectFilesRecursively(this.baseFilesPath); allFiles.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); return allFiles; } catch (error) { console.error( `[FileService] Error listing files recursively from ${this.baseFilesPath}:`, error, ); return []; } } public async deleteFile({ filePath }: { filePath: string }): Promise { await fs.promises.mkdir(this.baseFilesPath, { recursive: true }); const safeFilePath = this.getSafeFilePath(filePath); if (!(await this.exists(safeFilePath))) { console.log( `[FileService] File ${safeFilePath} not found on disk during delete operation. Skipping.`, ); return; } try { const stats = await fs.promises.stat(safeFilePath); if (!stats.isFile()) { console.warn(`[FileService] Path ${safeFilePath} is not a file. Skipping delete.`); return; } await fs.promises.unlink(safeFilePath); console.log(`[FileService] File deleted: ${safeFilePath}`); this.debouncedCreateArchive(); } catch (unlinkError) { console.error(`Error unlinking file ${safeFilePath}:`, unlinkError); throw unlinkError; } return; } public async cleanupFiles(): Promise { console.log(`[FileService cleanupFiles] Starting cleanup for directory: ${this.baseFilesPath}`); this.debouncedCreateArchive.cancel(); try { const archivePath = path.join(this.prebuiltArchiveDir, "files.zip"); if (fs.existsSync(archivePath)) { await fs.promises.unlink(archivePath); console.log(`[FileService cleanupFiles] Deleted archive file: ${archivePath}`); } const archiveDir = await fs.promises.readdir(this.prebuiltArchiveDir).catch(() => []); for (const file of archiveDir) { if (file.startsWith("files-") && file.endsWith(".zip.tmp")) { const tempFilePath = path.join(this.prebuiltArchiveDir, file); await fs.promises.unlink(tempFilePath).catch((err) => { console.error( `[FileService cleanupFiles] Error deleting temp archive ${tempFilePath}:`, err, ); }); } } } catch (err: any) { console.error(`[FileService cleanupFiles] Error cleaning up archive files:`, err); } try { const files = await fs.promises.readdir(this.baseFilesPath); for (const file of files) { await fs.promises.rm(path.join(this.baseFilesPath, file), { recursive: true, force: true, }); } console.log( `[FileService cleanupFiles] Cleared contents of directory: ${this.baseFilesPath}`, ); } catch (err: any) { if (err.code !== "ENOENT") { console.error( `[FileService cleanupFiles] Error cleaning directory ${this.baseFilesPath}:`, err, ); } } console.log(`[FileService cleanupFiles] Files cleaned. Creating empty archive.`); this.debouncedCreateArchive(); } public getBaseFilesPath(): string { return this.baseFilesPath; } public async getPrebuiltArchivePath(): Promise { return this.prebuiltArchivePath; } private _createArchive(): Promise { return new Promise(async (resolvePromise, rejectPromise) => { if (this.isArchiving) { console.warn( `[_createArchive] Warning: Archiving process initiated while another is already in progress. This might lead to conflicts if not handled by caller.`, ); } this.isArchiving = true; console.log(`[_createArchive] Starting archive creation`); const tempArchivePath = path.join(this.prebuiltArchiveDir, `files-${Date.now()}.zip.tmp`); const finalArchivePath = path.join(this.prebuiltArchiveDir, "files.zip"); try { await fs.promises.mkdir(this.prebuiltArchiveDir, { recursive: true }); } catch (mkdirError) { console.error( `[_createArchive] Error creating archive directory ${this.prebuiltArchiveDir}:`, mkdirError, ); this.isArchiving = false; return rejectPromise(mkdirError); } const output = fs.createWriteStream(tempArchivePath); const archive = archiver("zip", { zlib: { level: 9 } }); let errorOccurredStream = false; const operationCleanup = async ( success: boolean, archivePath: string | null = null, error?: any, ) => { this.isArchiving = false; if (!success && tempArchivePath && (await this.exists(tempArchivePath))) { try { await fs.promises.unlink(tempArchivePath); console.log("[_createArchive] Cleaned up temporary archive file due to error."); } catch (unlinkErr) { console.error( "[_createArchive] Failed to clean up temp archive file after error:", unlinkErr, ); } } if (success && archivePath) { resolvePromise(archivePath); } else { rejectPromise(error || new Error("Archiving failed due to an unknown reason.")); } }; output.on("close", async () => { if (errorOccurredStream) { console.log( "[_createArchive] Output stream closed after an error was emitted and handled.", ); return; } try { if (await this.exists(finalArchivePath)) { await fs.promises.unlink(finalArchivePath); } await fs.promises.rename(tempArchivePath, finalArchivePath); console.log( `[_createArchive] Archive successfully created: ${finalArchivePath}, size: ${archive.pointer()} bytes`, ); operationCleanup(true, finalArchivePath); } catch (renameError) { console.error("[_createArchive] Error renaming temporary archive file:", renameError); operationCleanup(false, null, renameError); } }); output.on("error", (err) => { console.error("[_createArchive] Archive output stream error:", err); errorOccurredStream = true; if (!output.writableFinished) { output.destroy(); } operationCleanup(false, null, err); }); archive.on("warning", (err) => { if (err.code === "ENOENT") { console.warn(`[_createArchive] Archiving warning (ENOENT): ${err.message}`); } else { console.error("[_createArchive] Archiving warning:", err); } }); archive.on("error", (err) => { console.error("[_createArchive] Archiving process error (archive.on('error')):", err); errorOccurredStream = true; if (!output.writableFinished) { output.destroy(err instanceof Error ? err : new Error(String(err))); } operationCleanup(false, null, err); }); try { if (!(await this.exists(this.baseFilesPath))) { console.warn( `[_createArchive] Base directory ${this.baseFilesPath} does not exist. Creating empty archive.`, ); } else { const stats = await fs.promises.stat(this.baseFilesPath); if (!stats.isDirectory()) { console.error( `[_createArchive] Base path ${this.baseFilesPath} is not a directory. Creating empty archive.`, ); } else { const files = await fs.promises.readdir(this.baseFilesPath); if (files.length === 0) { console.log("[_createArchive] Base directory is empty. Creating empty archive."); } else { archive.directory(this.baseFilesPath, false); } } } archive.pipe(output); await archive.finalize(); } catch (err: any) { console.error("[_createArchive] Error during archive preparation or finalization:", err); errorOccurredStream = true; if (!output.writableFinished) { output.destroy(err instanceof Error ? err : new Error(String(err))); } operationCleanup(false, null, err); } }); } } ================================================ FILE: api/src/services/leveldb/localstorage.ts ================================================ import fs from "fs/promises"; import path from "path"; import os from "os"; import { Level } from "level"; import iconv from "iconv-lite"; import { copyDirectory } from "../../utils/leveldb.js"; /** * Decode a Chrome-encoded string. * - 0x00 prefix => UTF‑16‑LE encoded string * - 0x01 prefix => ISO‑8859‑1 encoded string */ function decodeString(raw: Buffer): { value: string; charset: string } { if (!raw || raw.length === 0) { throw new Error("Cannot decode empty buffer"); } const prefix = raw[0]; const payload = raw.subarray(1); if (prefix === 0) { try { const decoded = iconv.decode(payload, "utf16-le"); return { value: decoded, charset: "UTF-16-LE" }; } catch (err: unknown) { throw new Error(`Failed to decode UTF-16-LE: ${err}`); } } else if (prefix === 1) { return { value: payload.toString("latin1"), charset: "ISO-8859-1" }; } throw new Error(`Unknown string encoding prefix: ${prefix}`); } export interface StorageMetadata { storageKey: string; timestamp: Date; size: number; } export interface LocalStorageRecord { storageKey: string; scriptKey: string; charset: string; decoded: string; mime?: string; conversions?: string[]; jsonType?: string; value?: unknown; } export class LocalStoreDb { private db: Level; public records: LocalStorageRecord[] = []; private constructor(db: Level) { this.db = db; } public static async open(dir: string): Promise { try { const db = new Level(dir, { createIfMissing: false, keyEncoding: "buffer", valueEncoding: "buffer", } as any); // wait until open await db.open(); return new LocalStoreDb(db); } catch (err) { // Attempt fallback: copy to temp directory and open there const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "chrome-leveldb-")); await copyDirectory(dir, tmpDir); const db = new Level(tmpDir, { createIfMissing: false, keyEncoding: "buffer", valueEncoding: "buffer", } as any); await db.open(); return new LocalStoreDb(db); } } public async load(): Promise { const META_PREFIX = Buffer.from("META:"); const RECORD_PREFIX = Buffer.from("_"); for await (const [keyBuf, valueBuf] of this.db.iterator() as any) { const key: Buffer = keyBuf as Buffer; const value: Buffer = valueBuf as Buffer; if (key[0] === RECORD_PREFIX[0]) { // Remove prefix const withoutPrefix = key.subarray(1); const nullIndex = withoutPrefix.indexOf(0); if (nullIndex === -1) continue; const storageKeyBytes = withoutPrefix.subarray(0, nullIndex); const scriptKeyBytes = withoutPrefix.subarray(nullIndex + 1); const storageKey = storageKeyBytes.toString(); let scriptKeyDecoded: { value: string; charset: string }; let valueDecoded: { value: string; charset: string }; try { scriptKeyDecoded = decodeString(scriptKeyBytes); valueDecoded = decodeString(valueBuf); } catch { continue; } this.records.push({ storageKey, scriptKey: scriptKeyDecoded.value, charset: valueDecoded.charset, decoded: valueDecoded.value, }); } } } public close(): void { // @ts-ignore types mismatch but close exists if (this.db.status === "open") this.db.close().catch(() => {}); } } export class ChromeLocalStorageReader { /** * Reads a Chrome Local Storage LevelDB directory and returns a nested record of items * grouped by domain / storage key. */ public static async readLocalStorage( dir: string, ): Promise>> { const ldb = await LocalStoreDb.open(dir); try { await ldb.load(); const result: Record> = {}; for (const rec of ldb.records) { if (!result[rec.storageKey]) { result[rec.storageKey] = {}; } result[rec.storageKey][rec.scriptKey] = rec.decoded; } return result; } finally { ldb.close(); } } } ================================================ FILE: api/src/services/leveldb/sessionstorage.ts ================================================ import fs from "fs/promises"; import path from "path"; import os from "os"; import { Level } from "level"; import iconv from "iconv-lite"; import { fileTypeFromBuffer } from "file-type"; import { copyDirectory } from "../../utils/leveldb.js"; /** * Decode a UTF-16LE string */ function decodeUTF16LE(raw: Buffer): string { try { return iconv.decode(raw, "utf16-le"); } catch (err: unknown) { throw new Error(`Failed to decode UTF-16-LE: ${err}`); } } export interface SessionStorageRecord { mapId: number; origin: string; key: string; charset: string; decoded: string; mime?: string; conversions?: string[]; jsonType?: string; value?: unknown; } export class SessionStoreDb { private db: Level; public records: SessionStorageRecord[] = []; private mapIdToOrigin: Map = new Map(); private constructor(db: Level) { this.db = db; } public static async open(dir: string): Promise { try { const db = new Level(dir, { createIfMissing: false, keyEncoding: "buffer", valueEncoding: "buffer", } as any); // wait until open await db.open(); return new SessionStoreDb(db); } catch (err) { // Attempt fallback: copy to temp directory and open there const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "chrome-leveldb-")); await copyDirectory(dir, tmpDir); const db = new Level(tmpDir, { createIfMissing: false, keyEncoding: "buffer", valueEncoding: "buffer", } as any); await db.open(); return new SessionStoreDb(db); } } /** * Load namespace records to map from mapId to origin (hostname) */ private async loadNamespaceRecords(): Promise { const NAMESPACE_PREFIX = Buffer.from("namespace-"); for await (const [keyBuf, valueBuf] of this.db.iterator() as any) { const key: Buffer = keyBuf as Buffer; const value: Buffer = valueBuf as Buffer; if (key.indexOf(NAMESPACE_PREFIX) === 0) { const keyStr = key.toString(); // Format: "namespace--" const parts = keyStr.split("-"); if (parts.length < 3) continue; // Get hostname from the remaining parts (in case hostname contains dashes) const hostname = parts.slice(2).join("-").replace(/\/$/, ""); // Get map-id value const mapId = parseInt(value.toString(), 10); if (isNaN(mapId)) continue; this.mapIdToOrigin.set(mapId, hostname); } } } public async load(): Promise { // First load namespace records to build mapId to origin mapping await this.loadNamespaceRecords(); const MAP_PREFIX = Buffer.from("map-"); for await (const [keyBuf, valueBuf] of this.db.iterator() as any) { const key: Buffer = keyBuf as Buffer; const value: Buffer = valueBuf as Buffer; if (key.indexOf(MAP_PREFIX) === 0) { const withoutPrefix = key.subarray(MAP_PREFIX.length); const parts = withoutPrefix.toString().split("-", 2); if (parts.length !== 2) continue; const mapId = parseInt(parts[0], 10); if (isNaN(mapId)) continue; const keyStr = parts[1]; let valueDecoded: string; try { valueDecoded = decodeUTF16LE(value); } catch { continue; } // Look up the origin from the mapId const origin = this.mapIdToOrigin.get(mapId) || "unknown-origin"; this.records.push({ mapId, origin, key: keyStr, charset: "UTF-16-LE", decoded: valueDecoded, }); } } } public close(): void { // @ts-ignore types mismatch but close exists if (this.db.status === "open") this.db.close().catch(() => {}); } } /** * Process a session storage record to add metadata about its content */ export function processSessionRecord(record: SessionStorageRecord): SessionStorageRecord { const result = { ...record }; const buffer = Buffer.from(record.decoded); let mime = "application/octet-stream"; const conversions: string[] = []; let jsonType = ""; let value: unknown = null; // Try to parse as JSON try { value = JSON.parse(record.decoded); mime = "application/json"; if (value === null) { jsonType = "null"; } else if (Array.isArray(value)) { jsonType = "array"; } else if (typeof value === "object") { jsonType = "object"; } else if (typeof value === "string") { jsonType = "string"; } else if (typeof value === "number") { jsonType = "number"; } else if (typeof value === "boolean") { jsonType = "boolean"; } } catch { // Not valid JSON, try to determine file type try { const quoted = JSON.stringify(record.decoded); if (JSON.parse(quoted)) { value = quoted; mime = "text/plain"; conversions.push("JSON.stringify"); } } catch { const b64 = buffer.toString("base64"); value = JSON.stringify(b64); conversions.push("buffer.toString('base64')"); conversions.push("JSON.stringify"); // Try to detect MIME type fileTypeFromBuffer(buffer) .then((type) => { if (type) { result.mime = type.mime; } }) .catch(() => {}); } } result.mime = mime; result.conversions = conversions; result.jsonType = jsonType; result.value = value; return result; } export class ChromeSessionStorageReader { /** * Reads a Chrome Session Storage LevelDB directory and returns a nested record of items * grouped by origin (hostname). */ public static async readSessionStorage( dir: string, ): Promise>> { const sdb = await SessionStoreDb.open(dir); try { await sdb.load(); const result: Record> = {}; for (const rec of sdb.records) { if (!result[rec.origin]) { result[rec.origin] = {}; } result[rec.origin][rec.key] = rec.decoded; } return result; } finally { sdb.close(); } } } ================================================ FILE: api/src/services/selenium.service.ts ================================================ import { EventEmitter } from "events"; import { ChildProcess, spawn } from "child_process"; import { BrowserLauncherOptions, BrowserEvent, BrowserEventType } from "../types/index.js"; import path, { dirname } from "path"; import { FastifyBaseLogger } from "fastify"; import { fileURLToPath } from "url"; export class SeleniumService extends EventEmitter { private seleniumProcess: ChildProcess | null = null; private seleniumServerUrl: string = "http://localhost:4444"; private port: number = 4444; private launchOptions?: BrowserLauncherOptions; private logger: FastifyBaseLogger; constructor(logger: FastifyBaseLogger) { super(); this.logger = logger; } public async getChromeArgs(): Promise { const { options, userAgent } = this.launchOptions ?? {}; return [ "disable-dev-shm-usage", "no-sandbox", "enable-javascript", userAgent ? `user-agent=${userAgent}` : "", options?.proxyUrl ? `proxy-server=${options.proxyUrl}` : "", ...(options?.args?.map((arg) => (arg.startsWith("--") ? arg.slice(2) : arg)) || []), ].filter(Boolean); } public async launch(launchOptions: BrowserLauncherOptions): Promise { this.launchOptions = launchOptions; if (this.seleniumProcess) { await this.close(); } const projectRoot = path.resolve(dirname(fileURLToPath(import.meta.url)), "../../"); const seleniumServerPath = path.join(projectRoot, "selenium", "server", "selenium-server.jar"); const seleniumArgs = ["-jar", seleniumServerPath, "standalone"]; this.seleniumProcess = spawn("java", seleniumArgs); this.seleniumServerUrl = `http://localhost:${this.port}`; this.seleniumProcess.stdout?.on("data", (data) => { this.logger.info(`Selenium stdout: ${data}`); this.postLog({ type: BrowserEventType.Console, text: JSON.stringify({ type: BrowserEventType.Console, message: `${data}` }), timestamp: new Date(), }); }); this.seleniumProcess.stderr?.on("data", (data) => { this.logger.error(`Selenium stderr: ${data}`); this.postLog({ type: BrowserEventType.Error, text: JSON.stringify({ type: BrowserEventType.Error, error: `${data}` }), timestamp: new Date(), }); }); this.seleniumProcess.on("close", (code) => { this.logger.info(`Selenium process exited with code ${code}`); this.seleniumProcess = null; }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error("Selenium server failed to start within the timeout period")); }, 15000); // 15 seconds timeout this.seleniumProcess!.stdout?.on("data", (data) => { if (data.toString().includes("Started Selenium Standalone")) { clearTimeout(timeout); resolve(); } }); }); } public close(): void { if (this.seleniumProcess) { this.seleniumProcess.kill("SIGINT"); this.seleniumProcess = null; } } public getSeleniumServerUrl(): string { return this.seleniumServerUrl; } private async postLog(browserLog: BrowserEvent) { if (!this.launchOptions?.logSinkUrl) { return; } await fetch(this.launchOptions.logSinkUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(browserLog), }); } } ================================================ FILE: api/src/services/session.service.ts ================================================ import { FastifyBaseLogger } from "fastify"; import { mkdir } from "fs/promises"; import os from "os"; import path, { dirname } from "path"; import { fileURLToPath } from "url"; import { v4 as uuidv4 } from "uuid"; import { env } from "../env.js"; import { BrowserFingerprintWithHeaders } from "fingerprint-generator"; import { CredentialsOptions, SessionDetails } from "../modules/sessions/sessions.schema.js"; import { BrowserLauncherOptions, OptimizeBandwidthOptions } from "../types/index.js"; import { IProxyServer, ProxyServer } from "../utils/proxy.js"; import { getBaseUrl, getUrl } from "../utils/url.js"; import { CDPService } from "./cdp/cdp.service.js"; import { CookieData } from "./context/types.js"; import { FileService } from "./file.service.js"; import { SeleniumService } from "./selenium.service.js"; import { TimezoneFetcher } from "./timezone-fetcher.service.js"; import { deepMerge } from "../utils/context.js"; type Session = SessionDetails & { completion: Promise; complete: (value: void) => void; proxyServer: IProxyServer | undefined; }; const sessionStats = { duration: 0, eventCount: 0, timeout: 0, creditsUsed: 0, proxyTxBytes: 0, proxyRxBytes: 0, }; const defaultSession = { status: "idle" as SessionDetails["status"], websocketUrl: getBaseUrl("ws"), debugUrl: getUrl("v1/sessions/debug"), debuggerUrl: getUrl("v1/devtools/inspector.html"), sessionViewerUrl: getBaseUrl(), dimensions: { width: 1920, height: 1080 }, userAgent: "", isSelenium: false, proxy: "", solveCaptcha: false, }; export type ProxyFactory = (proxyUrl: string) => Promise | IProxyServer; export class SessionService { private logger: FastifyBaseLogger; private cdpService: CDPService; private seleniumService: SeleniumService; private fileService: FileService; private timezoneFetcher: TimezoneFetcher; public proxyFactory: ProxyFactory = (proxyUrl) => new ProxyServer(proxyUrl); public pastSessions: Session[] = []; public activeSession: Session; constructor(config: { cdpService: CDPService; seleniumService: SeleniumService; fileService: FileService; logger: FastifyBaseLogger; }) { this.cdpService = config.cdpService; this.seleniumService = config.seleniumService; this.fileService = config.fileService; this.logger = config.logger; this.timezoneFetcher = new TimezoneFetcher(config.logger); this.activeSession = { id: uuidv4(), createdAt: new Date().toISOString(), ...defaultSession, ...sessionStats, userAgent: this.cdpService.getUserAgent() ?? "", dimensions: this.cdpService.getDimensions(), completion: Promise.resolve(), complete: () => {}, proxyServer: undefined, }; } public async startSession(options: { sessionId?: string; proxyUrl?: string; userAgent?: string; sessionContext?: { cookies?: CookieData[]; localStorage?: Record>; }; isSelenium?: boolean; fingerprint?: BrowserFingerprintWithHeaders; logSinkUrl?: string; userDataDir?: string; persist?: boolean; blockAds?: boolean; optimizeBandwidth?: boolean | OptimizeBandwidthOptions; extensions?: string[]; timezone?: string; dimensions?: { width: number; height: number }; extra?: Record>; credentials: CredentialsOptions; skipFingerprintInjection?: boolean; userPreferences?: Record; deviceConfig?: { device: "desktop" | "mobile" }; headless?: boolean; dangerouslyLogRequestDetails?: boolean; }): Promise { const { sessionId, proxyUrl, userAgent, sessionContext, extensions, logSinkUrl, dimensions, fingerprint, isSelenium, blockAds, optimizeBandwidth, extra, credentials, skipFingerprintInjection, userPreferences, deviceConfig, headless, dangerouslyLogRequestDetails, } = options; // start fetching timezone as early as possible let timezonePromise: Promise; if (options.timezone) { timezonePromise = Promise.resolve(options.timezone); } else { timezonePromise = this.timezoneFetcher.getTimezone( proxyUrl, env.DEFAULT_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone, ); } // If dimensions not provided, get from CDP service const finalDimensions = dimensions || this.cdpService.getDimensions(); await this.resetSessionInfo({ id: sessionId || uuidv4(), status: "live", proxy: proxyUrl, solveCaptcha: false, dimensions: finalDimensions, isSelenium, }); const userDataDir = options.userDataDir || options.persist === true ? path.join(dirname(fileURLToPath(import.meta.url)), "..", "..", "user-data-dir") : env.CHROME_USER_DATA_DIR || path.join(os.tmpdir(), "steel-chrome"); await mkdir(userDataDir, { recursive: true }); if (proxyUrl) { this.activeSession.proxyServer = await this.proxyFactory(proxyUrl); await this.activeSession.proxyServer.listen(); } const defaultUserPreferences = { plugins: { always_open_pdf_externally: true, plugins_disabled: ["Chrome PDF Viewer"], }, }; const mergedUserPreferences = userPreferences ? deepMerge(defaultUserPreferences, userPreferences) : defaultUserPreferences; // Normalize optimizeBandwidth: true => enable all flags (except lists) const normalizeOptimizeBandwidth = ( value: boolean | OptimizeBandwidthOptions | undefined, ): OptimizeBandwidthOptions | undefined => { if (value === true) { return { blockImages: true, blockMedia: true, blockStylesheets: true }; } if (value && typeof value === "object") { return { ...value }; } return undefined; }; const normalizedOptimize = normalizeOptimizeBandwidth(optimizeBandwidth); const browserLauncherOptions: BrowserLauncherOptions = { options: { headless: headless ?? env.CHROME_HEADLESS, proxyUrl: this.activeSession.proxyServer?.url, }, sessionContext, userAgent, blockAds, fingerprint, optimizeBandwidth: normalizedOptimize, extensions: extensions || [], logSinkUrl, timezone: timezonePromise, dimensions, userDataDir, userPreferences: mergedUserPreferences, extra, credentials, skipFingerprintInjection, deviceConfig, dangerouslyLogRequestDetails, }; if (isSelenium) { await this.cdpService.shutdown(); await this.seleniumService.launch(browserLauncherOptions); Object.assign(this.activeSession, { websocketUrl: "", debugUrl: "", sessionViewerUrl: "", userAgent: userAgent || "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", dimensions: this.cdpService.getDimensions(), }); return this.activeSession; } else { await this.cdpService.startNewSession(browserLauncherOptions); Object.assign(this.activeSession, { websocketUrl: getBaseUrl("ws"), debugUrl: getUrl("v1/sessions/debug"), debuggerUrl: getUrl("v1/devtools/inspector.html"), sessionViewerUrl: getBaseUrl(), userAgent: this.cdpService.getUserAgent() || "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", dimensions: this.cdpService.getDimensions(), }); } return this.activeSession; } public async endSession(): Promise { this.activeSession.complete(); this.activeSession.status = "released"; this.activeSession.duration = new Date().getTime() - new Date(this.activeSession.createdAt).getTime(); if (this.activeSession.proxyServer) { this.activeSession.proxyTxBytes = this.activeSession.proxyServer.txBytes; this.activeSession.proxyRxBytes = this.activeSession.proxyServer.rxBytes; } if (this.activeSession.isSelenium) { this.seleniumService.close(); await this.cdpService.launch(); } else { await this.cdpService.endSession(); } const releasedSession = this.activeSession; await this.resetSessionInfo({ id: uuidv4(), status: "idle", }); this.pastSessions.push(releasedSession); return releasedSession; } private async resetSessionInfo(overrides?: Partial): Promise { this.activeSession.complete(); await this.activeSession.proxyServer?.close(true); this.activeSession.proxyServer = undefined; const { promise, resolve } = Promise.withResolvers(); this.activeSession = { id: uuidv4(), ...defaultSession, ...overrides, ...sessionStats, userAgent: this.cdpService.getUserAgent() ?? "", createdAt: new Date().toISOString(), completion: promise, complete: resolve, proxyServer: undefined, }; return this.activeSession; } public setProxyFactory(factory: ProxyFactory) { this.proxyFactory = factory; } } ================================================ FILE: api/src/services/timezone-fetcher.service.ts ================================================ import { FastifyBaseLogger } from "fastify"; import axios, { AxiosError } from "axios"; import { HttpsProxyAgent } from "https-proxy-agent"; import { SocksProxyAgent } from "socks-proxy-agent"; import { env } from "../env.js"; export interface TimezoneFetchResult { timezone: string | null; error?: string; service?: string; } interface TimezoneService { name: string; url: string; parseTimezone: (data: any) => string | null; } export class TimezoneFetcher { private logger: FastifyBaseLogger; private fetchPromises: Map> = new Map(); private readonly FETCH_TIMEOUT = 2000; private readonly services: TimezoneService[]; constructor(logger: FastifyBaseLogger) { this.logger = logger; // Use timezone service URL from env, default to ipinfo.io const serviceUrl = env.TIMEZONE_SERVICE_URL || "https://ipinfo.io/json"; this.services = [ { name: new URL(serviceUrl).hostname, url: serviceUrl, parseTimezone: (data) => data?.timezone || null, }, ]; } private startFetch(proxyUrl?: string): Promise { const cacheKey = proxyUrl || "direct"; const existing = this.fetchPromises.get(cacheKey); if (existing) { return existing; } const fetchPromise = this.fetchTimezoneInternal(proxyUrl); this.fetchPromises.set(cacheKey, fetchPromise); fetchPromise.finally(() => { this.fetchPromises.delete(cacheKey); }); return fetchPromise; } public async getTimezone(proxyUrl?: string, fallback?: string): Promise { const startTime = Date.now(); try { const result = await this.startFetch(proxyUrl); if (result.timezone) { const duration = Date.now() - startTime; this.logger.info( { timezone: result.timezone, service: result.service, duration_ms: duration, has_proxy: !!proxyUrl, }, `[TimezoneFetcher] Successfully fetched timezone`, ); return result.timezone; } } catch (error) { const duration = Date.now() - startTime; this.logger.warn( { duration_ms: duration, has_proxy: !!proxyUrl, }, `[TimezoneFetcher] Failed to fetch timezone: ${error}`, ); } const fallbackTimezone = fallback || Intl.DateTimeFormat().resolvedOptions().timeZone; this.logger.info(`[TimezoneFetcher] Using fallback timezone: ${fallbackTimezone}`); return fallbackTimezone; } private async fetchTimezoneInternal(proxyUrl?: string): Promise { const agent = proxyUrl ? proxyUrl.startsWith("socks") ? new SocksProxyAgent(proxyUrl) : new HttpsProxyAgent(proxyUrl) : undefined; const servicePromises = this.services.map(async (service) => { try { const response = await axios.get(service.url, { httpAgent: agent, httpsAgent: agent, timeout: this.FETCH_TIMEOUT, }); const timezone = service.parseTimezone(response.data); if (timezone) { return { timezone, service: service.name, }; } else { throw new Error(`No timezone found in response from ${service.name}`); } } catch (error: unknown) { const errorMessage = error instanceof AxiosError ? error.message : String(error); this.logger.warn(`[TimezoneFetcher] ${service.name} failed: ${errorMessage}`); throw new Error(`${service.name}: ${errorMessage}`); } }); try { const result = await Promise.any(servicePromises); return result; } catch (error: unknown) { const errorMessage = error instanceof AggregateError ? `All services failed: ${error.errors.map((e) => e.message).join(", ")}` : error instanceof Error ? error.message : String(error); const context = proxyUrl ? `with proxy` : "with direct connection"; this.logger.warn(`[TimezoneFetcher] All services failed ${context}: ${errorMessage}`); return { timezone: null, error: errorMessage, }; } } } ================================================ FILE: api/src/services/websocket-registry.service.ts ================================================ import { WebSocketHandler, WebSocketHandlerRegistry } from "../types/websocket.js"; export class WebSocketRegistryService implements WebSocketHandlerRegistry { public handlers = new Map(); registerHandler(handler: WebSocketHandler): void { this.handlers.set(handler.path, handler); } getHandler(path: string): WebSocketHandler | undefined { return this.handlers.get(path); } matchHandler(url: string): WebSocketHandler | undefined { // TODO: use path-to-regexp or find-my-way to match the path // Find the first handler whose path matches the start of the URL for (const [path, handler] of this.handlers.entries()) { if (url.startsWith(path)) { return handler; } } return undefined; } } ================================================ FILE: api/src/steel-browser-plugin.ts ================================================ import fastifyView from "@fastify/view"; import { FastifyPluginAsync } from "fastify"; import fp from "fastify-plugin"; import path, { dirname } from "node:path"; import browserInstancePlugin from "./plugins/browser.js"; import browserSessionPlugin from "./plugins/browser-session.js"; import browserWebSocket from "./plugins/browser-socket/browser-socket.js"; import customBodyParser from "./plugins/custom-body-parser.js"; import fileStoragePlugin from "./plugins/file-storage.js"; import requestLogger from "./plugins/request-logger.js"; import openAPIPlugin from "./plugins/schemas.js"; import seleniumPlugin from "./plugins/selenium.js"; import { actionsRoutes, cdpRoutes, filesRoutes, logsRoutes, seleniumRoutes, sessionsRoutes, } from "./routes.js"; import { fileURLToPath } from "node:url"; import ejs from "ejs"; import type { CDPService } from "./services/cdp/cdp.service.js"; import type { BrowserLauncherOptions } from "./types/browser.js"; import { WebSocketHandler } from "./types/websocket.js"; import { WebSocketRegistryService } from "./services/websocket-registry.service.js"; import { SessionService } from "./services/session.service.js"; import { LogStorage } from "./services/cdp/instrumentation/storage/log-storage.interface.js"; // We need to redeclare any decorators from within the plugin that we want to expose declare module "fastify" { interface FastifyInstance { steelBrowserConfig: SteelBrowserConfig; cdpService: CDPService; sessionService: SessionService; webSocketRegistry: WebSocketRegistryService; registerCDPLaunchHook: (hook: (config: BrowserLauncherOptions) => Promise | void) => void; registerCDPShutdownHook: ( hook: (config: BrowserLauncherOptions | null) => Promise | void, ) => void; } interface LogStorageInterface extends LogStorage {} } export interface SteelBrowserConfig { fileStorage?: { maxSizePerSession?: number; }; customWsHandlers?: WebSocketHandler[]; logging?: { enableStorage?: boolean; storagePath?: string; enableConsoleLogging?: boolean; enableLogsRoutes?: boolean; }; } const steelBrowserPlugin: FastifyPluginAsync = async (fastify, opts) => { fastify.decorate("steelBrowserConfig", opts); // Plugins await fastify.register(fastifyView, { engine: { ejs, }, root: path.join(dirname(fileURLToPath(import.meta.url)), "templates"), }); await fastify.register(requestLogger); await fastify.register(openAPIPlugin); await fastify.register(fileStoragePlugin); await fastify.register(browserInstancePlugin); await fastify.register(seleniumPlugin); await fastify.register(browserWebSocket, { customHandlers: opts.customWsHandlers, }); await fastify.register(customBodyParser); await fastify.register(browserSessionPlugin); // Routes await fastify.register(actionsRoutes, { prefix: "/v1" }); await fastify.register(sessionsRoutes, { prefix: "/v1" }); await fastify.register(cdpRoutes, { prefix: "/v1" }); await fastify.register(seleniumRoutes); await fastify.register(filesRoutes, { prefix: "/v1" }); const enableLogsRoutes = opts.logging?.enableLogsRoutes ?? true; if (enableLogsRoutes) { await fastify.register(logsRoutes, { prefix: "/v1/logs" }); } }; export default fp(steelBrowserPlugin, { name: "steel-browser", fastify: "5.x", }); ================================================ FILE: api/src/telemetry/noop.ts ================================================ import type { Span } from "@opentelemetry/api"; export const noopSpan: Span = { spanContext() { return { traceId: "", spanId: "", traceFlags: 0, isRemote: false, }; }, setAttribute() { return this; }, setAttributes() { return this; }, addEvent() { return this; }, addLink() { return this; }, addLinks() { return this; }, setStatus() { return this; }, updateName() { return this; }, end() {}, isRecording() { return false; }, recordException() {}, }; ================================================ FILE: api/src/telemetry/tracer.ts ================================================ import type { Span, SpanOptions } from "@opentelemetry/api"; import { noopSpan } from "./noop.js"; let otel: typeof import("@opentelemetry/api") | undefined; try { otel = await import("@opentelemetry/api"); } catch (err: any) { if (err?.code !== "MODULE_NOT_FOUND" && err?.code !== "ERR_MODULE_NOT_FOUND") { throw err; } } interface TracerOptions extends SpanOptions { spanName?: string; tracerName?: string; } export const tracer = { startActiveSpan unknown>( name: string, fn: F, opts?: Omit, ): ReturnType { if (!otel) { return fn(noopSpan) as ReturnType; } const { tracerName, ...options } = opts ?? {}; const rawTracer = otel.trace.getTracer(tracerName ?? "steel"); return rawTracer.startActiveSpan(name, options ?? {}, (span: Span) => { try { const result = fn(span); if (result instanceof Promise) { return result .catch((error: Error) => { span.recordException(error); span.setStatus({ code: otel.SpanStatusCode.ERROR, message: error.message, }); throw error; }) .finally(() => { span.end(); }) as ReturnType; } span.setStatus({ code: otel.SpanStatusCode.OK }); span.end(); return result as ReturnType; } catch (error: any) { span.recordException(error); span.setStatus({ code: otel.SpanStatusCode.ERROR, message: error.message, }); span.end(); throw error; } }); }, factory(tracerName: string) { return { startActiveSpan unknown>( name: string, fn: F, opts?: Omit, ): ReturnType { return tracer.startActiveSpan(name, fn, { ...opts, tracerName }); }, }; }, }; export function traceable( target: Object, propertyKey: string, descriptor: PropertyDescriptor, ): void; export function traceable(opts?: TracerOptions): MethodDecorator; export function traceable( targetOrOpts?: any, propertyKey?: string, descriptor?: PropertyDescriptor, ): any { // Used as @traceable if (typeof targetOrOpts === "object" && propertyKey !== undefined && descriptor !== undefined) { return createDecorator()(targetOrOpts, propertyKey, descriptor); } // Used as @traceable({...}) return createDecorator(targetOrOpts as TracerOptions | undefined); } function createDecorator(opts?: TracerOptions) { const { spanName, tracerName, ...options } = opts ?? {}; return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { if (!otel) { return descriptor; } const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { const tracername = tracerName ?? toKebabCase(target.constructor.name); const name = spanName ?? `${target.constructor.name}.${propertyKey}`; return tracer.startActiveSpan( name, () => { return originalMethod.apply(this, args); }, { ...options, tracerName: tracername, }, ); }; }; } function toKebabCase(str: string) { return str .replace(/([a-z0-9])([A-Z])/g, "$1-$2") .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2") .toLowerCase(); } ================================================ FILE: api/src/templates/live-session-streamer.ejs ================================================ > Steel Session Player
<% if (showControls) { %>
<% if (!singlePageMode) { %>
Session Offline
<% } %>
>
<% } %>
================================================ FILE: api/src/types/browser.ts ================================================ import type { BrowserEventType } from "./enums.js"; import type { CookieData, IndexedDBDatabase, LocalStorageData, SessionStorageData, } from "../services/context/types.js"; import { BrowserFingerprintWithHeaders } from "fingerprint-generator"; import type { CredentialsOptions } from "../modules/sessions/sessions.schema.js"; export type OptimizeBandwidthOptions = { blockImages?: boolean; blockMedia?: boolean; blockStylesheets?: boolean; blockHosts?: string[]; blockUrlPatterns?: string[]; }; export interface BrowserLauncherOptions { options: BrowserServerOptions; req?: Request; stealth?: boolean; sessionContext?: { cookies?: CookieData[]; localStorage?: Record; sessionStorage?: Record; indexedDB?: Record; }; userAgent?: string; extensions?: string[]; logSinkUrl?: string; blockAds?: boolean; fingerprint?: BrowserFingerprintWithHeaders; optimizeBandwidth?: boolean | OptimizeBandwidthOptions; customHeaders?: Record; timezone?: Promise; dimensions?: { width: number; height: number; } | null; userDataDir?: string; userPreferences?: Record; extra?: Record>; credentials?: CredentialsOptions; skipFingerprintInjection?: boolean; deviceConfig?: { device: "desktop" | "mobile" }; dangerouslyLogRequestDetails?: boolean; } export interface BrowserServerOptions { args?: string[]; chromiumSandbox?: boolean; devtools?: boolean; downloadsPath?: string; headless?: boolean; ignoreDefaultArgs?: boolean | string[]; proxyUrl?: string; timeout?: number; tracesDir?: string; } export type BrowserEvent = { type: BrowserEventType; text: string; timestamp: Date; }; ================================================ FILE: api/src/types/casting.ts ================================================ export type MouseEvent = { type: "mouseEvent"; pageId: string; event: { type: "mousePressed" | "mouseReleased" | "mouseWheel" | "mouseMoved"; x: number; y: number; button: "none" | "left" | "middle" | "right"; modifiers: number; clickCount?: number; deltaX?: number; deltaY?: number; }; }; export type KeyEvent = { type: "keyEvent"; pageId: string; event: { type: "keyDown" | "keyUp" | "char"; text?: string; code: string; key: string; keyCode: number; modifiers?: number; }; }; export type NavigationEvent = { type: "navigation"; pageId: string; event: { url?: string; action?: "back" | "forward" | "refresh"; }; }; export type CloseTabEvent = { type: "closeTab"; pageId: string; }; export type ClipboardWriteEvent = { type: "clipboardWrite"; pageId: string; event: { text: string; }; }; export type ClipboardReadEvent = { type: "clipboardRead"; pageId: string; }; export type GetSelectedTextEvent = { type: "getSelectedText"; pageId: string; }; export type PageInfo = { id: string; url: string; title: string; favicon: string | null; }; ================================================ FILE: api/src/types/enums.ts ================================================ import { BrowserServerOptions } from "./browser.js"; export enum ScrapeFormat { HTML = "html", READABILITY = "readability", CLEANED_HTML = "cleaned_html", MARKDOWN = "markdown", } export enum BrowserEventType { Request = "Request", Navigation = "Navigation", Console = "Console", PageError = "PageError", RequestFailed = "RequestFailed", Response = "Response", Error = "Error", BrowserError = "BrowserError", Recording = "Recording", ScreencastFrame = "ScreencastFrame", CDPCommand = "CDPCommand", CDPCommandResult = "CDPCommandResult", CDPEvent = "CDPEvent", ResponseBody = "ResponseBody", } export enum EmitEvent { Log = "log", PageId = "pageId", Recording = "recording", } ================================================ FILE: api/src/types/fastify.d.ts ================================================ import { FastifyRequest } from "fastify"; import { CDPService } from "../services/cdp/cdp.service.js"; import { SessionService } from "../services/session.service.js"; import { SeleniumService } from "../services/selenium.service.js"; import { Page } from "puppeteer-core"; import { FileService } from "../services/file.service.js"; declare module "fastify" { interface FastifyRequest {} interface FastifyInstance { seleniumService: SeleniumService; sessionService: SessionService; fileService: FileService; } } ================================================ FILE: api/src/types/index.ts ================================================ export * from "./enums.js"; export * from "./browser.js"; export * from "./websocket.js"; ================================================ FILE: api/src/types/turndown.d.ts ================================================ declare module "@joplin/turndown" { export { default } from "turndown"; export { Options } from "turndown"; export { Node } from "turndown"; } declare module "@joplin/turndown-plugin-gfm" { export const gfm: any; export const tables: any; export const strikethrough: any; } ================================================ FILE: api/src/types/websocket.ts ================================================ import { IncomingMessage } from "http"; import { Duplex } from "stream"; import { WebSocketServer } from "ws"; import { FastifyInstance } from "fastify"; export interface WebSocketHandlerContext { fastify: FastifyInstance; wss: WebSocketServer; params: Record; } export interface WebSocketHandler { path: string; handler: ( request: IncomingMessage, socket: Duplex, head: Buffer, context: WebSocketHandlerContext, ) => Promise | void; } export interface WebSocketHandlerRegistry { handlers: Map; registerHandler: (handler: WebSocketHandler) => void; getHandler: (path: string) => WebSocketHandler | undefined; matchHandler: (url: string) => WebSocketHandler | undefined; } ================================================ FILE: api/src/utils/browser.ts ================================================ import fs from "fs"; import path from "path"; import { Page } from "puppeteer-core"; import { env } from "../env.js"; export const getChromeExecutablePath = () => { if (env.CHROME_EXECUTABLE_PATH) { const executablePath = env.CHROME_EXECUTABLE_PATH; const normalizedPath = path.normalize(executablePath); if (!fs.existsSync(normalizedPath)) { console.warn(`Your custom chrome executable at ${normalizedPath} does not exist`); } else { return executablePath; } } if (process.platform === "win32") { const programFilesPath = `${process.env["ProgramFiles"]}\\Google\\Chrome\\Application\\chrome.exe`; const programFilesX86Path = `C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe`; if (fs.existsSync(programFilesPath)) { return programFilesPath; } else if (fs.existsSync(programFilesX86Path)) { return programFilesX86Path; } } if (process.platform === "darwin") { return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; } return "/usr/bin/chromium"; }; export async function installMouseHelper(page: Page, device: string) { await page.evaluateOnNewDocument((deviceType) => { // Install mouse helper only for top-level frame. if (window !== window.parent) return; window.addEventListener( "DOMContentLoaded", () => { if (deviceType === "desktop") { // Desktop mode: show regular arrow cursor const CURSOR_ID = "__cursor__"; if (document.getElementById(CURSOR_ID)) return; const cursor = document.createElement("div"); cursor.id = CURSOR_ID; Object.assign(cursor.style, { position: "fixed", top: "0px", left: "0px", width: "20px", height: "20px", backgroundImage: `url("data:image/svg+xml;utf8,")`, backgroundSize: "cover", pointerEvents: "none", zIndex: "99999", transform: "translate(-2px, -2px)", }); document.body.appendChild(cursor); document.addEventListener("mousemove", (e) => { cursor.style.top = e.clientY + "px"; cursor.style.left = e.clientX + "px"; }); } else { // Mobile mode: show circular touch indicator const box = document.createElement("puppeteer-mouse-pointer"); const styleElement = document.createElement("style"); styleElement.innerHTML = ` puppeteer-mouse-pointer { pointer-events: none; position: absolute; top: 0; z-index: 10000; left: 0; width: 20px; height: 20px; background: rgba(0,0,0,.4); border: 1px solid white; border-radius: 10px; margin: -10px 0 0 -10px; padding: 0; transition: background .2s, border-radius .2s, border-color .2s; } puppeteer-mouse-pointer.button-1 { transition: none; background: rgba(0,0,0,0.9); } puppeteer-mouse-pointer.button-2 { transition: none; border-color: rgba(0,0,255,0.9); } puppeteer-mouse-pointer.button-3 { transition: none; border-radius: 4px; } puppeteer-mouse-pointer.button-4 { transition: none; border-color: rgba(255,0,0,0.9); } puppeteer-mouse-pointer.button-5 { transition: none; border-color: rgba(0,255,0,0.9); } `; document.head.appendChild(styleElement); document.body.appendChild(box); document.addEventListener( "mousemove", (event) => { box.style.left = event.pageX + "px"; box.style.top = event.pageY + "px"; updateButtons(event.buttons); }, true, ); document.addEventListener( "mousedown", (event) => { updateButtons(event.buttons); box.classList.add("button-" + event.which); }, true, ); document.addEventListener( "mouseup", (event) => { updateButtons(event.buttons); box.classList.remove("button-" + event.which); }, true, ); function updateButtons(buttons) { for (let i = 0; i < 5; i++) // @ts-ignore box.classList.toggle("button-" + i, buttons & (1 << i)); } } }, false, ); }, device); } export function filterHeaders(headers: Record) { const headersToRemove = [ "accept-encoding", "accept", "cache-control", "pragma", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "sec-fetch-user", "upgrade-insecure-requests", ]; const filteredHeaders = { ...headers }; headersToRemove.forEach((header) => { delete filteredHeaders[header]; }); return filteredHeaders; } ================================================ FILE: api/src/utils/casting.ts ================================================ import { Page } from "puppeteer-core"; import { NavigationEvent } from "../types/casting.js"; import { normalizeUrl } from "./url.js"; export const navigatePage = async ( event: NavigationEvent["event"], targetPage: Page, ): Promise => { if (event.action === "back") { await targetPage.goBack(); } else if (event.action === "forward") { await targetPage.goForward(); } else if (event.action === "refresh") { await targetPage.reload(); } else if (event.url) { const formattedUrl = normalizeUrl(event.url) || event.url; await targetPage.goto(formattedUrl); } }; export const getPageTitle = async (page: Page): Promise => { try { return await page.title(); } catch (error) { return "Untitled"; } }; export const getPageFavicon = async (page: Page): Promise => { try { return await page.evaluate(() => { const iconLink = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]'); if (iconLink) { const href = iconLink.getAttribute("href"); if (href?.startsWith("http")) return href; if (href?.startsWith("//")) return window.location.protocol + href; if (href?.startsWith("/")) return window.location.origin + href; return window.location.origin + "/" + href; } return null; }); } catch (error) { return null; } }; ================================================ FILE: api/src/utils/context.ts ================================================ import { Page } from "puppeteer-core"; import { SessionData, IndexedDBDatabase, IndexedDBObjectStore, IndexedDBRecord, SessionStorageData, LocalStorageData, } from "../services/context/types.js"; import { FastifyBaseLogger } from "fastify"; import { BrowserLauncherOptions } from "../types/index.js"; import path from "path"; /** * Extract storage data for a single origin * @param client CDP session * @param origin Origin to process * @returns Storage data for the origin */ export async function extractStorageForPage( page: Page, logger: FastifyBaseLogger, ): Promise { const result: SessionData = { localStorage: {}, sessionStorage: {}, indexedDB: {}, }; try { // Skip pages that aren't valid or don't have a proper URL const url = page.url(); if (!url || !url.startsWith("http")) { return result; } // Extract origin and domain from URL const origin = new URL(url).origin; const domain = new URL(url).hostname; const client = await page.target().createCDPSession(); try { // Check if the page has a valid main frame const { frameTree } = await client .send("Page.getFrameTree") .catch(() => ({ frameTree: null })); if (!frameTree) { logger.debug(`[CDPService] Page has no valid frame tree for ${domain}`); return result; } // Get localStorage using CDP try { const localStorageResponse = await client.send("DOMStorage.getDOMStorageItems", { storageId: { securityOrigin: origin, isLocalStorage: true }, }); if (localStorageResponse?.entries?.length) { result.localStorage![domain] = {}; for (const [key, value] of localStorageResponse.entries) { result.localStorage![domain][key] = value; } } } catch (err) { // Lower log level to avoid flooding logs with expected errors logger.trace(`[CDPService] Could not get localStorage for ${domain}: ${err}`); } // Get sessionStorage (note: only works for active pages) try { const sessionStorageResponse = await client.send("DOMStorage.getDOMStorageItems", { storageId: { securityOrigin: origin, isLocalStorage: false }, }); if (sessionStorageResponse?.entries?.length) { result.sessionStorage![domain] = {}; for (const [key, value] of sessionStorageResponse.entries) { result.sessionStorage![domain][key] = value; } } } catch (err) { // Lower log level to avoid flooding logs with expected errors logger.trace(`[CDPService] Could not get sessionStorage for ${domain}: ${err}`); } // Get IndexedDB databases try { const dbResponse = await client.send("IndexedDB.requestDatabaseNames", { securityOrigin: origin, }); const databaseNames = dbResponse?.databaseNames || []; if (databaseNames.length) { result.indexedDB![domain] = []; // Process each database for (let dbIndex = 0; dbIndex < databaseNames.length; dbIndex++) { const dbName = databaseNames[dbIndex]; // Create a properly structured database object const database: IndexedDBDatabase = { id: dbIndex, name: dbName, data: [], }; // Get database schema const dbSchemaResponse = await client.send("IndexedDB.requestDatabase", { securityOrigin: origin, databaseName: dbName, }); // Access object stores safely const objectStores = dbSchemaResponse?.databaseWithObjectStores?.objectStores || []; // Process each object store for (let storeIndex = 0; storeIndex < objectStores.length; storeIndex++) { const store = objectStores[storeIndex]; // Create a properly structured object store const objectStore: IndexedDBObjectStore = { id: storeIndex, name: store.name, records: [], }; // Paginate through all records let hasMoreData = true; let skipCount = 0; const pageSize = 1000; while (hasMoreData) { const dataResponse = await client.send("IndexedDB.requestData", { securityOrigin: origin, databaseName: dbName, objectStoreName: store.name, indexName: "", // Empty string means use primary key skipCount, pageSize, }); // Add the retrieved data const objectStoreData = dataResponse?.objectStoreDataEntries || []; if (objectStoreData.length) { // Map the data to the correct record format const records: IndexedDBRecord[] = objectStoreData.map((entry) => ({ key: entry.key, value: entry.value, // TODO: Add blob files })); objectStore.records.push(...records); } // Check if we need to continue pagination hasMoreData = !!dataResponse?.hasMore; skipCount += objectStoreData.length; // Safety check to prevent infinite loops if (objectStoreData.length === 0) break; } // Add the object store to the database database.data.push(objectStore); } // Add the database to the result result.indexedDB![domain].push(database); } } } catch (err) { // Lower log level to avoid flooding logs with expected errors logger.trace(`[CDPService] Could not get IndexedDB for ${domain}: ${err}`); } } finally { // Always ensure the client session is detached await client.detach().catch(() => {}); } } catch (err) { logger.warn(`[CDPService] Error extracting storage for page: ${err}`); } return result; } // Create our frameNavigated handler export const handleFrameNavigated = async ( frame: any, storageByOrigin: Map< string, { localStorage?: LocalStorageData; sessionStorage?: SessionStorageData; indexedDB?: IndexedDBDatabase[]; } >, logger: FastifyBaseLogger, ) => { // Only process top-level frames if (frame.parentFrame()) return; try { const url = frame.url(); if (!url || !url.startsWith("http")) return; // Extract the origin from the URL const origin = new URL(url).origin; // Check if we have storage for this origin const storage = storageByOrigin.get(origin); if (!storage) return; logger.debug(`[CDPService] Injecting storage for navigated origin: ${origin}`); // Set localStorage if available if (storage.localStorage) { await frame.evaluate((items) => { for (const [key, value] of Object.entries(items)) { try { if (typeof value === "string") { localStorage.setItem(key, value); } } catch (e) { console.error(`Error setting localStorage: ${e}`); } } }, storage.localStorage); } // Set sessionStorage if available if (storage.sessionStorage) { await frame.evaluate((items) => { for (const [key, value] of Object.entries(items)) { try { if (typeof value === "string") { sessionStorage.setItem(key, value); } } catch (e) { console.error(`Error setting sessionStorage: ${e}`); } } }, storage.sessionStorage); } // Set IndexedDB if available if (storage.indexedDB && storage.indexedDB.length > 0) { for (const database of storage.indexedDB) { if (!database.name || !database.data) continue; // Create a store map for this database const storeMap = {}; for (const store of database.data) { if (!store.name || !store.records || store.records.length === 0) continue; storeMap[store.name] = store.records.map((record) => { try { // Parse the key and value if they're stored as strings const parsedKey = typeof record.key === "string" ? JSON.parse(record.key) : record.key; const parsedValue = typeof record.value === "string" ? JSON.parse(record.value) : record.value; return { key: parsedKey, value: parsedValue }; } catch (e) { // Fall back to original values if parsing fails return { key: record.key, value: record.value }; } }); } if (Object.keys(storeMap).length === 0) continue; await frame.evaluate( async (dbName, stores) => { return new Promise((resolve, reject) => { try { const openRequest = indexedDB.open(dbName, 1); openRequest.onupgradeneeded = function (event) { const db = (event.target as IDBOpenDBRequest).result; // Create object stores from our data for (const storeName of Object.keys(stores)) { if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName, { keyPath: "key" }); } } }; openRequest.onsuccess = function (event) { const db = (event.target as IDBOpenDBRequest).result; let completedStores = 0; const totalStores = Object.keys(stores).length; for (const [storeName, storeData] of Object.entries(stores)) { if (!db.objectStoreNames.contains(storeName)) { // Skip if object store doesn't exist and can't be created completedStores++; continue; } const transaction = db.transaction(storeName, "readwrite"); const objectStore = transaction.objectStore(storeName); // Add all items for (const item of storeData as any[]) { try { objectStore.put(item); } catch (e) { console.error(`Error adding item to IndexedDB: ${e}`); } } transaction.oncomplete = function () { completedStores++; if (completedStores === totalStores) { resolve(true); } }; transaction.onerror = function (err) { console.error(`Transaction error: ${err}`); completedStores++; if (completedStores === totalStores) { resolve(false); } }; } // Handle case with no stores if (totalStores === 0) { resolve(true); } }; openRequest.onerror = function (event) { reject(`Error opening IndexedDB: ${(event.target as IDBOpenDBRequest).error}`); }; } catch (e) { reject(`IndexedDB restore error: ${e}`); } }); }, database.name, storeMap, ); } } } catch (err) { logger.error(`[CDPService] Error injecting storage during navigation: ${err}`); } }; /** * Organizes session storage data by origin for efficient lookup * @param context Session context data from BrowserLauncherOptions * @returns Map of origins to their storage data */ export function groupSessionStorageByOrigin( context?: BrowserLauncherOptions["sessionContext"], ): Map< string, { localStorage?: LocalStorageData; sessionStorage?: SessionStorageData; indexedDB?: IndexedDBDatabase[]; } > { const result = new Map< string, { localStorage?: LocalStorageData; sessionStorage?: SessionStorageData; indexedDB?: IndexedDBDatabase[]; } >(); if (!context) return result; if (context.localStorage) { for (const [domain, storage] of Object.entries(context.localStorage)) { if (!result.has(domain)) { result.set(domain, {}); } result.get(domain)!.localStorage = storage; } } if (context.sessionStorage) { for (const [domain, storage] of Object.entries(context.sessionStorage)) { if (!result.has(domain)) { result.set(domain, {}); } result.get(domain)!.sessionStorage = storage; } } if (context.indexedDB) { for (const [domain, databases] of Object.entries(context.indexedDB)) { if (!result.has(domain)) { result.set(domain, {}); } result.get(domain)!.indexedDB = databases; } } return result; } /** * Helper to get Chrome profile paths in a cross-platform way * Takes into account different Chrome profile directory structures */ export function getProfilePath(userDataDir: string, ...pathSegments: string[]): string { // Chrome profile directories vary by platform and version // Both "Default" and "Profile 1" are standard locations const possibleProfileDirs = ["Default", "Profile 1"]; // First check if the userDataDir already includes a profile directory const dirName = path.basename(userDataDir); if (possibleProfileDirs.includes(dirName)) { // userDataDir already points to a profile directory return path.join(userDataDir, ...pathSegments); } const defaultPath = path.join(userDataDir, "Default", ...pathSegments); return defaultPath; } /** * Deep merge two objects, with the second object taking precedence. * Arrays are replaced entirely, not merged. */ export function deepMerge(target: T, source: Partial): T { if (typeof target !== "object" || target === null) { return source as T; } if (typeof source !== "object" || source === null) { return target; } const result = { ...target }; for (const key in source) { if (source.hasOwnProperty(key)) { const sourceValue = source[key]; const targetValue = (result as any)[key]; if (typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue)) { (result as any)[key] = deepMerge(targetValue, sourceValue); } else { (result as any)[key] = sourceValue; } } } return result; } ================================================ FILE: api/src/utils/errors.ts ================================================ export function getErrors(e: unknown) { let error: string; if (typeof e === "string") { error = e; } else if (e instanceof Error) { error = e.message; } else { error = "Unknown error"; } return error; } ================================================ FILE: api/src/utils/extensions.ts ================================================ import fs from "fs"; import path, { dirname } from "path"; import { fileURLToPath } from "url"; export async function getExtensionPaths(extensionNames: string[]): Promise { const extensionsDir = path.join( dirname(fileURLToPath(import.meta.url)), "..", "..", "extensions", ); try { await fs.promises.access(extensionsDir); } catch { console.warn("Extensions directory does not exist"); return []; } const allExtensions = await fs.promises.readdir(extensionsDir); const candidatePaths = extensionNames .filter((name) => allExtensions.includes(name)) .map((dir) => path.join(extensionsDir, dir)); const validationResults = await Promise.all( candidatePaths.map(async (fullPath) => { try { await fs.promises.access(fullPath); return { path: fullPath, valid: true }; } catch { console.warn(`Extension directory ${fullPath} does not exist`); return { path: fullPath, valid: false }; } }), ); return validationResults.filter((result) => result.valid).map((result) => result.path); } ================================================ FILE: api/src/utils/leveldb.ts ================================================ import path from "path"; import fs from "fs/promises"; /** * Utility to copy a LevelDB directory to a temporary path if opening directly fails (e.g. database lock). */ export async function copyDirectory(src: string, dest: string): Promise { await fs.mkdir(dest, { recursive: true }); const entries = await fs.readdir(src, { withFileTypes: true }); await Promise.all( entries.map(async (entry) => { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { await copyDirectory(srcPath, destPath); } else if (entry.isFile()) { const data = await fs.readFile(srcPath); await fs.writeFile(destPath, data); } }), ); } ================================================ FILE: api/src/utils/logging.ts ================================================ export const updateLog = async (logUrl: string, log: any) => { try { const response = await fetch(logUrl, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify(log), }); if (!response.ok) { const error = await response.text(); console.error("Failed to update log", error); } } catch (e: unknown) { console.error(e); } }; ================================================ FILE: api/src/utils/passthough-proxy.ts ================================================ import http, { IncomingHttpHeaders } from "node:http"; import { PrepareRequestFunctionOpts, PrepareRequestFunctionResult } from "proxy-chain"; // Headers that are hop-by-hop and should not be forwarded RFC 2616, Section 13.5.1 export const hopByHopHeaders = new Set([ "connection", "proxy-authenticate", "proxy-authorization", "keep-alive", "te", "trailers", "transfer-encoding", "upgrade", ]); /** * Creates a simple http proxy which reverse proxies the original request to the original host. * There's an issue with proxy-chain's implementation causing corruption in our internals requests */ export const PassthroughServer = http.createServer((clientReq, clientRes) => { const targetUrl = new URL(clientReq.url ?? "/", `http://${clientReq.headers.host}`); const proxyHeaders: IncomingHttpHeaders = {}; for (const [key, value] of Object.entries(clientReq.headers)) { const lowerKey = key.toLowerCase(); if (!hopByHopHeaders.has(lowerKey)) { proxyHeaders[lowerKey] = value; } } proxyHeaders["host"] = targetUrl.host; proxyHeaders["x-forwarded-for"] = clientReq.socket.remoteAddress || ""; proxyHeaders["x-forwarded-proto"] = "http"; proxyHeaders["x-forwarded-host"] = clientReq.headers.host || ""; const proxyReq = http.request( { method: clientReq.method, headers: proxyHeaders, hostname: targetUrl.hostname, port: targetUrl.port, path: targetUrl.pathname + targetUrl.search, }, (proxyRes) => { const clientResHeaders = {}; for (const [key, value] of Object.entries(proxyRes.headers)) { if (!hopByHopHeaders.has(key.toLowerCase())) { clientResHeaders[key] = value; } } clientRes.writeHead(proxyRes.statusCode ?? 500, clientResHeaders); proxyRes.pipe(clientRes); }, ); proxyReq.on("error", (err) => { console.error(`Proxy error for ${targetUrl}:`, err); if (!clientRes.headersSent) { clientRes.writeHead(502); } clientRes.end("Proxy error: Could not connect to the target service."); }); clientReq.on("error", (err) => { console.error(`Client request error:`, err); proxyReq.destroy(); }); clientReq.pipe(proxyReq); }); type Result = [err: Error, result: null] | [err: null, result: T]; /** * There's an issue with proxy-chain's handling of chunked requests when doing a direct passthrough. * This workaround forwards the requests manually and returns the response */ export const makePassthrough = function ({ request, hostname, port, }: PrepareRequestFunctionOpts): NonNullable< PrepareRequestFunctionResult["customResponseFunction"] > { return async () => { const [err, proxyRes]: Result = await new Promise((resolve) => { const forward = http.request( { hostname, port, method: request.method, path: request.url, headers: request.headers, }, (res) => resolve([null, res]), ); forward.on("error", (err) => resolve([err, null])); request.pipe(forward); }); if (err) { console.error(`Request failed "${err.name}": ${err.message}`); throw err; } const chunks: Buffer[] = []; for await (const chunk of proxyRes) chunks.push(chunk); const body = Buffer.concat(chunks); const headers: IncomingHttpHeaders = {}; for (const [k, v] of Object.entries(proxyRes.headers)) { if (!hopByHopHeaders.has(k.toLowerCase()) && v !== undefined) { headers[k] = Array.isArray(v) ? v.join(",") : v; } } return { statusCode: proxyRes.statusCode ?? 500, headers: proxyRes.headers as Record, body, }; }; }; ================================================ FILE: api/src/utils/proxy.ts ================================================ import { env } from "../env.js"; import { SessionService } from "../services/session.service.js"; import { makePassthrough, PassthroughServer } from "./passthough-proxy.js"; import { Server } from "proxy-chain"; export interface IProxyServer { readonly url: string; readonly upstreamProxyUrl: string; readonly txBytes: number; readonly rxBytes: number; listen(): Promise; close(force?: boolean): Promise; } export class ProxyServer extends Server implements IProxyServer { public url: string; public upstreamProxyUrl: string; public txBytes = 0; public rxBytes = 0; private hostConnections = new Set(); constructor(proxyUrl: string) { super({ port: 0, prepareRequestFunction: (options) => { const { connectionId, hostname } = options; const internalBypassTests = new Set(["0.0.0.0", process.env.HOST]); if (env.PROXY_INTERNAL_BYPASS) { for (const host of env.PROXY_INTERNAL_BYPASS.split(",")) { internalBypassTests.add(host.trim()); } } const isInternalBypass = internalBypassTests.has(hostname); if (isInternalBypass) { this.hostConnections.add(connectionId); return { customConnectServer: PassthroughServer, customResponseFunction: makePassthrough(options), }; } return { requestAuthentication: false, upstreamProxyUrl: proxyUrl, }; }, }); this.on("connectionClosed", ({ connectionId, stats }) => { if (stats && !this.hostConnections.has(connectionId)) { this.txBytes += stats.trgTxBytes; this.rxBytes += stats.trgRxBytes; } this.hostConnections.delete(connectionId); }); this.url = `http://127.0.0.1:${this.port}`; this.upstreamProxyUrl = proxyUrl; } async listen(): Promise { await super.listen(); this.url = `http://127.0.0.1:${this.port}`; } } ================================================ FILE: api/src/utils/requests.ts ================================================ const AD_HOSTS = [ // Ad Networks & Services "doubleclick.net", "adservice.google.com", "googlesyndication.com", "google-analytics.com", "adnxs.com", "rubiconproject.com", "advertising.com", "adtechus.com", "quantserve.com", "scorecardresearch.com", "casalemedia.com", "moatads.com", "criteo.com", "amazon-adsystem.com", "serving-sys.com", "adroll.com", "chartbeat.com", "sharethrough.com", "indexww.com", "mediamath.com", "adsystem.com", "adservice.com", "adnxs.com", "ads-twitter.com", // Analytics & Tracking "hotjar.com", "analytics.google.com", "mixpanel.com", "kissmetrics.com", "googletagmanager.com", // Microsoft Clarity "clarity.ms", "www.clarity.ms", "static.clarity.ms", // Ad Exchanges "openx.net", "pubmatic.com", "bidswitch.net", "taboola.com", "outbrain.com", // Social Media Tracking "facebook.com/tr/", "connect.facebook.net", "platform.twitter.com", "ads.linkedin.com", ]; const RE_IMAGE_EXT = /\.(jpg|jpeg|png|webp|svg|ico)(\?.*)?$/i; const RE_VIDEO_EXT = /\.(mp4|m4s|m3u8|ts|webm|gif)(\?.*)?$/i; const RE_RANGE = /range=\d+-\d+/i; export function tryParseUrl(url: string): URL | null { try { return new URL(url); } catch { return null; } } export function isAdRequest(parsed: URL): boolean { const { hostname } = parsed; return AD_HOSTS.some((adHost) => hostname === adHost || hostname.endsWith(`.${adHost}`)); } export function isImageRequest(parsed: URL): boolean { return RE_IMAGE_EXT.test(parsed.pathname); } export function isHeavyMediaRequest(parsed: URL): boolean { const { pathname, searchParams } = parsed; if (RE_VIDEO_EXT.test(pathname)) return true; const isRange = searchParams.has("range") || RE_RANGE.test(parsed.href); return isRange && pathname.includes("/avf/"); } export function isHostBlocked(parsed: URL, blockedHosts?: string[]): boolean { if (!blockedHosts?.length) return false; const { hostname } = parsed; return blockedHosts.some((h) => hostname === h || hostname.endsWith(`.${h}`)); } export function compileUrlPatterns(patterns: string[]): RegExp[] { return patterns.map((pattern) => { try { return new RegExp( `^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`, "i", ); } catch { // Fallback: escape the entire pattern for literal matching return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"); } }); } export function isUrlMatchingPatterns(url: string, compiledPatterns?: RegExp[]): boolean { if (!compiledPatterns?.length) return false; return compiledPatterns.some((re) => re.test(url)); } ================================================ FILE: api/src/utils/retry.ts ================================================ import { FastifyBaseLogger } from "fastify"; import { BaseLaunchError, ConfigurationError, LaunchTimeoutError, ResourceError, } from "../services/cdp/errors/launch-errors.js"; export interface RetryOptions { maxAttempts: number; baseDelayMs: number; maxDelayMs: number; backoffMultiplier: number; jitterMs?: number; } export interface RetryResult { result: T; attempt: number; totalDuration: number; } export class RetryError extends Error { public readonly attempts: number; public readonly lastError: Error; public readonly allErrors: Error[]; constructor(attempts: number, lastError: Error, allErrors: Error[]) { super(`Failed after ${attempts} attempts. Last error: ${lastError.message}`); this.name = "RetryError"; this.attempts = attempts; this.lastError = lastError; this.allErrors = allErrors; } } /** * Retry utility with exponential backoff and jitter for retryable launch errors */ export class RetryManager { private logger: FastifyBaseLogger; private defaultOptions: RetryOptions = { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 5000, backoffMultiplier: 2, jitterMs: 250, }; constructor(logger: FastifyBaseLogger) { this.logger = logger; } /** * Execute a function with retry logic for retryable errors */ async executeWithRetry( operation: () => Promise, operationName: string, options: Partial = {}, ): Promise> { const opts = { ...this.defaultOptions, ...options }; const errors: Error[] = []; const startTime = Date.now(); for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) { try { this.logger.info( `[RetryManager] ${operationName} - Attempt ${attempt}/${opts.maxAttempts}`, ); const result = await operation(); const totalDuration = Date.now() - startTime; if (attempt > 1) { this.logger.info( `[RetryManager] ${operationName} succeeded on attempt ${attempt}/${opts.maxAttempts} after ${totalDuration}ms`, ); } return { result, attempt, totalDuration, }; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); errors.push(err); const isRetryable = this.isErrorRetryable(err); const isLastAttempt = attempt === opts.maxAttempts; this.logger.warn( { error: err.message, isRetryable, isLastAttempt, errorType: err instanceof BaseLaunchError ? err.type : "unknown", }, `[RetryManager] ${operationName} failed on attempt ${attempt}/${opts.maxAttempts}`, ); if (!isRetryable || isLastAttempt) { if (!isRetryable) { this.logger.error( `[RetryManager] ${operationName} failed with non-retryable error: ${err.message}`, ); throw err; // Throw original error for non-retryable errors } else { this.logger.error( `[RetryManager] ${operationName} failed after ${opts.maxAttempts} attempts`, ); throw new RetryError(attempt, err, errors); } } // Calculate delay with exponential backoff and jitter const baseDelay = opts.baseDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1); const jitter = opts.jitterMs ? Math.random() * opts.jitterMs : 0; const delay = Math.min(baseDelay + jitter, opts.maxDelayMs); this.logger.info( `[RetryManager] Waiting ${Math.round(delay)}ms before retry ${attempt + 1}/${ opts.maxAttempts }`, ); await this.sleep(delay); } } // This should never be reached, but TypeScript needs it throw new RetryError(opts.maxAttempts, errors[errors.length - 1], errors); } private isErrorRetryable(error: Error): boolean { if ( error instanceof ConfigurationError || error instanceof ResourceError || error instanceof LaunchTimeoutError ) { return false; } if (error instanceof BaseLaunchError) { return error.isRetryable; } // For non-categorized errors, we'll be conservative and not retry. return false; } private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } createRetryWrapper( method: (...args: T) => Promise, operationName: string, options: Partial = {}, ): (...args: T) => Promise { return async (...args: T): Promise => { const result = await this.executeWithRetry(() => method(...args), operationName, options); return result.result; }; } } ================================================ FILE: api/src/utils/schema.ts ================================================ import { ZodType, z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; export type Models = { readonly [K in Key]: ZodType; }; export type BuildJsonSchemasOptions = { readonly $id?: string; readonly target?: `jsonSchema7` | `openApi3`; readonly errorMessages?: boolean; }; export type SchemaKey = M extends Models ? Key & string : never; export type SchemaKeyOrDescription = | SchemaKey | { readonly description: string; readonly key: SchemaKey; }; export type $Ref = (key: SchemaKeyOrDescription) => { readonly $ref: string; readonly description?: string; }; export type JsonSchema = { readonly $id: string; }; export type BuildJsonSchemasResult = { readonly schemas: JsonSchema[]; readonly $ref: $Ref; }; export const buildJsonSchemas = ( models: M, opts: BuildJsonSchemasOptions = {}, ): BuildJsonSchemasResult => { const zodSchema = z.object(models); const zodJsonSchema = zodToJsonSchema(zodSchema, { target: "openApi3", $refStrategy: "none", errorMessages: opts.errorMessages, }); const cleanedSchemas = Object.entries( //@ts-ignore zodJsonSchema.properties as { [key: string]: any }, ).reduce((acc, [key, value]) => { return [...acc, { $id: key, title: key, ...value }]; }, [] as JsonSchema[]); const $ref: $Ref = (key) => { const $ref = `${typeof key === `string` ? key : key.key}#`; return typeof key === `string` ? { $ref, } : { $ref, description: key.description, }; }; return { schemas: cleanedSchemas, $ref, }; }; ================================================ FILE: api/src/utils/scrape/cleanHtml.ts ================================================ import { JSDOM } from "jsdom"; export const cleanHtml = (html: string): string => { const blacklistedElements = new Set([ "head", "title", "meta", "script", "style", "path", "svg", "br", "hr", "link", "object", "embed", ]); const blacklistedAttributes = [ "style", "ping", "src", "item.*", "aria.*", "js.*", "data-.*", "role", "tabindex", "onerror", ]; const dom = new JSDOM(html); const document = dom.window.document; // Remove blacklisted elements blacklistedElements.forEach((tag) => { const elements = document.querySelectorAll(tag); elements.forEach((element) => { element.remove(); }); }); // Remove blacklisted attributes const elements = document.querySelectorAll("*"); elements.forEach((element) => { blacklistedAttributes.forEach((attrPattern) => { const regex = new RegExp(`^${attrPattern}$`); Array.from(element.attributes).forEach((attr: any) => { if (regex.test(attr.name)) { element.removeAttribute(attr.name); } }); }); }); // Remove empty elements elements.forEach((element) => { if (!element.hasAttributes() && element.textContent?.trim() === "") { element.remove(); } }); const sourceCode = document.documentElement.outerHTML; return sourceCode; }; ================================================ FILE: api/src/utils/scrape/htmlToMarkdown.ts ================================================ import { applyFixes } from "markdownlint"; import { lint } from "markdownlint/promise"; import Turndown from "turndown"; import highlightedCodeBlock from "./plugins/highlightedCodeBlock.js"; import inlineLink from "./plugins/inlineLink.js"; import strikethrough from "./plugins/strikethrough.js"; import tables from "./plugins/table.js"; import taskListItems from "./plugins/taskListItems.js"; const turndownService = new Turndown({ headingStyle: "atx", codeBlockStyle: "fenced", bulletListMarker: "-", emDelimiter: "*", strongDelimiter: "**", linkStyle: "inlined", preformattedCode: false, }).use([highlightedCodeBlock, strikethrough, taskListItems, inlineLink, tables]); export const htmlToMarkdown = async (html: string): Promise => { let markdown: string; markdown = turndownService.turndown(html).trim(); markdown = newlinesToSpacesInLinks(markdown); markdown = await lintMarkdown(markdown); return markdown; }; const lintMarkdown = async (md: string) => { const lintResult = await lint({ strings: { md }, config: { "no-trailing-punctuation": false, }, resultVersion: 3, }); const fixes = lintResult["md"].filter((error) => error.fixInfo); if (fixes.length > 0) { return applyFixes(md, fixes).trim(); } return md.trim(); }; const newlinesToSpacesInLinks = (markdownContent: string) => { const linkRegex = /\[([\s\S]*?)\]\(([\s\S]*?)\)/g; return markdownContent.replace(linkRegex, (_match, linkText, linkUrl) => { const cleanedText = linkText.trim().replace(/\s+/g, " "); const cleanedUrl = linkUrl.trim().replace(/\s+/g, ""); return `[${cleanedText}](${cleanedUrl})`; }); }; ================================================ FILE: api/src/utils/scrape/index.ts ================================================ export { cleanHtml } from "./cleanHtml.js"; export { htmlToMarkdown } from "./htmlToMarkdown.js"; export { getDefuddleContent } from "./readability.js"; export { transformHtml } from "./transformHtml.js"; ================================================ FILE: api/src/utils/scrape/pdfToHtml.ts ================================================ import { load as loadHtml } from "cheerio"; function parsePdfDate(pdfDate?: string | null): string | null { if (!pdfDate) return null; // PDF date format: D:YYYYMMDDHHmmSSOHH'mm' // Example: D:20240102153045-08'00' const m = pdfDate.match( /^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?([Zz]|([+\-])(\d{2})'?(\d{2})'?)?$/, ); if (!m) return null; const [_, y, mo = "01", d = "01", h = "00", mi = "00", s = "00", z, sign, tzH, tzM] = m; const yyyy = y; const MM = mo.padStart(2, "0"); const dd = d.padStart(2, "0"); const HH = h.padStart(2, "0"); const MMm = mi.padStart(2, "0"); const SS = s.padStart(2, "0"); let offset = "Z"; if (z && z.toUpperCase() !== "Z" && tzH && tzM) { offset = `${sign}${tzH}:${tzM}`; } // Build ISO string const iso = `${yyyy}-${MM}-${dd}T${HH}:${MMm}:${SS}${offset}`; const date = new Date(iso); return isNaN(date.getTime()) ? null : date.toISOString(); } type HtmlLikeMetadata = { title: string | null; language: string | null; urlSource: string | null; timestamp: string; description: string | null; keywords: string | null; author: string | null; ogTitle: string | null; ogDescription: string | null; ogImage: string | null; ogUrl: string | null; ogSiteName: string | null; articleAuthor: string | null; publishedTime: string | null; modifiedTime: string | null; canonical: string | null; favicon: string | null; jsonLd: any[]; statusCode: number; }; export function extractLinksFromConvertedHtml(html: string): { url: string; text: string }[] { const $ = loadHtml(html); return $("a[href]") .map((_, a) => { const url = $(a).attr("href") || ""; const text = $(a).text()?.trim() || ""; return { url, text }; }) .get(); } export function buildHtmlLikeMetadataFromPdf( pdfMeta: any, opts: { urlSource?: string | null; statusCode?: number; htmlForFallback?: string | null }, ): HtmlLikeMetadata { const { urlSource = null, statusCode = 200, htmlForFallback = null } = opts; // Try to get a title from meta, fallback to in converted HTML let htmlTitle: string | null = null; if (htmlForFallback) { const $ = loadHtml(htmlForFallback); const t = $("title").first().text()?.trim(); htmlTitle = t || null; } const title = pdfMeta?.title || htmlTitle || null; const author = pdfMeta?.author || null; const description = pdfMeta?.subject || null; // Keywords might be array or string depending on library let keywords: string | null = null; if (Array.isArray(pdfMeta?.keywords)) { keywords = pdfMeta.keywords.join(", "); } else if (typeof pdfMeta?.keywords === "string") { keywords = pdfMeta.keywords; } // XMP/DC language if exposed; often not present const language = pdfMeta?.language || pdfMeta?.["dc:language"] || null; const publishedTime = parsePdfDate(pdfMeta?.creationDate || pdfMeta?.CreationDate || pdfMeta?.["xmp:CreateDate"]) || null; const modifiedTime = parsePdfDate(pdfMeta?.modDate || pdfMeta?.ModDate || pdfMeta?.["xmp:ModifyDate"]) || null; let origin: string | null = null; let host: string | null = null; if (urlSource) { try { const u = new URL(urlSource); origin = u.origin; host = u.hostname; } catch {} } return { title, language, urlSource, timestamp: new Date().toISOString(), description, keywords, author, ogTitle: title, ogDescription: description, ogImage: null, ogUrl: urlSource, ogSiteName: host, articleAuthor: author, publishedTime, modifiedTime, canonical: urlSource, favicon: origin ? `${origin}/favicon.ico` : null, jsonLd: [], statusCode, }; } ================================================ FILE: api/src/utils/scrape/plugins/highlightedCodeBlock.ts ================================================ // Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js import TurndownService from "@joplin/turndown"; const highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/; export default function highlightedCodeBlock(turndownService: TurndownService) { turndownService.addRule("highlightedCodeBlock", { filter: function (node) { const firstChild = node.firstChild as HTMLElement; return ( node.nodeName === "DIV" && highlightRegExp.test(node.className) && firstChild && firstChild.nodeName === "PRE" ); }, replacement: function (content, node, options) { const className = (node as HTMLElement).className || ""; const language = (className.match(highlightRegExp) || [null, ""])[1]; return ( "\n\n" + options.fence + language + "\n" + (node.firstChild as HTMLElement).textContent + "\n" + options.fence + "\n\n" ); }, }); } ================================================ FILE: api/src/utils/scrape/plugins/inlineLink.ts ================================================ // Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js import TurndownService from "@joplin/turndown"; export default function inlineLink(turndownService: TurndownService) { turndownService.addRule("inlineLink", { filter: function (node, options) { return ( options.linkStyle === "inlined" && node.nodeName === "A" && !!node.getAttribute("href") ); }, replacement: function (content, node) { const href = (node as HTMLElement).getAttribute("href")?.trim(); const title = (node as HTMLElement).title ? ' "' + (node as HTMLElement).title + '"' : ""; return "[" + content.trim() + "](" + href + title + ")\n"; }, }); } ================================================ FILE: api/src/utils/scrape/plugins/strikethrough.ts ================================================ // Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js import TurndownService from "@joplin/turndown"; export default function taskListItems(turndownService: TurndownService) { turndownService.addRule("taskListItems", { filter: function (node) { const parent = node.parentNode as HTMLElement; const grandparent = parent.parentNode as HTMLElement; return ( (node as HTMLInputElement).type === "checkbox" && (parent.nodeName === "LI" || // Handles the case where the label contains the checkbox. For example, // <label><input ...> ...label text...</label> (parent.nodeName === "LABEL" && grandparent && grandparent.nodeName === "LI")) ); }, replacement: function (content, node) { return ((node as HTMLInputElement).checked ? "[x]" : "[ ]") + " "; }, }); } ================================================ FILE: api/src/utils/scrape/plugins/table.ts ================================================ // Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js import TurndownService from "@joplin/turndown"; import { isCodeBlock } from "./utilities.js"; var indexOf = Array.prototype.indexOf; var every = Array.prototype.every; var rules: Record<string, any> = {}; var alignMap = { left: ":---", right: "---:", center: ":---:" }; // We need to cache the result of tableShouldBeSkipped() as it is expensive. // Caching it means we went from about 9000 ms for rendering down to 90 ms. // Fixes https://github.com/laurent22/joplin/issues/6736 const tableShouldBeSkippedCache_ = new WeakMap(); function getAlignment(node) { return node ? (node.getAttribute("align") || node.style.textAlign || "").toLowerCase() : ""; } function getBorder(alignment) { return alignment ? alignMap[alignment] : "---"; } function getColumnAlignment(table, columnIndex) { var votes = { left: 0, right: 0, center: 0, "": 0, }; var align = ""; for (var i = 0; i < table.rows.length; ++i) { var row = table.rows[i]; if (columnIndex < row.childNodes.length) { var cellAlignment = getAlignment(row.childNodes[columnIndex]); ++votes[cellAlignment]; if (votes[cellAlignment] > votes[align]) { align = cellAlignment; } } } return align; } function extractTextFromCell(cellNode: HTMLElement): string { const uiComponentTags = new Set(["BUTTON", "SVG", "INPUT", "SELECT", "TEXTAREA", "FORM"]); function getTextContent(node: Node): string { if (node.nodeType === node.TEXT_NODE) { return node.textContent || ""; } if (node.nodeType === node.ELEMENT_NODE) { const element = node as HTMLElement; if (uiComponentTags.has(element.tagName)) { return ""; } let text = ""; for (const child of element.childNodes) { text += getTextContent(child); } return text; } return ""; } return getTextContent(cellNode).trim().replace(/\s+/g, " "); } rules.tableCell = { filter: ["th", "td"], replacement: function (content, node) { if (tableShouldBeSkipped(nodeParentTable(node))) return content; // Extract only text content from complex UI components const cleanContent = extractTextFromCell(node as HTMLElement); return cell(cleanContent, node); }, }; rules.tableRow = { filter: "tr", replacement: function (content, node) { const parentTable = nodeParentTable(node); if (tableShouldBeSkipped(parentTable)) return content; var borderCells = ""; if (isHeadingRow(node)) { const colCount = tableColCount(parentTable); for (var i = 0; i < colCount; i++) { const childNode = i < node.childNodes.length ? node.childNodes[i] : null; var border = getBorder(getColumnAlignment(parentTable, i)); borderCells += cell(border, childNode, i); } } return "\n" + content + (borderCells ? "\n" + borderCells : ""); }, }; rules.table = { filter: function (node: Node, options: any) { return node.nodeName === "TABLE"; }, replacement: function (content: string, node: Node) { // Only convert tables that can result in valid Markdown // Other tables are kept as HTML using `keep` (see below). if (tableShouldBeHtml(node)) { return `\n\n${(node as HTMLElement).outerHTML}\n\n`; } else { if (tableShouldBeSkipped(node)) return content; // Ensure there are no blank lines content = content.replace(/\n+/g, "\n"); // If table has no heading, add an empty one so as to get a valid Markdown table var secondLine: string[] | string = content.trim().split("\n"); if (secondLine.length >= 2) secondLine = secondLine[1]; var secondLineIsDivider = /\| :?---/.test(secondLine as string); var columnCount = tableColCount(node); var emptyHeader = ""; if (columnCount && !secondLineIsDivider) { emptyHeader = "|" + " |".repeat(columnCount) + "\n" + "|"; for (var columnIndex = 0; columnIndex < columnCount; ++columnIndex) { emptyHeader += " " + getBorder(getColumnAlignment(node, columnIndex)) + " |"; } } const captionContent = (node as HTMLTableElement).caption ? (node as HTMLTableElement).caption?.textContent || "" : ""; const caption = captionContent ? `${captionContent}\n\n` : ""; const tableContent = `${emptyHeader}${content}`.trimStart(); return `\n\n${caption}${tableContent}\n\n`; } }, }; rules.tableCaption = { filter: ["caption"], replacement: () => "", }; rules.tableColgroup = { filter: ["colgroup", "col"], replacement: () => "", }; rules.tableSection = { filter: ["thead", "tbody", "tfoot"], replacement: function (content) { return content; }, }; // A tr is a heading row if: // - the parent is a THEAD // - or if its the first child of the TABLE or the first TBODY (possibly // following a blank THEAD) // - and every cell is a TH function isHeadingRow(tr) { var parentNode = tr.parentNode; return ( parentNode.nodeName === "THEAD" || (parentNode.firstChild === tr && (parentNode.nodeName === "TABLE" || isFirstTbody(parentNode)) && every.call(tr.childNodes, function (n) { return n.nodeName === "TH"; })) ); } function isFirstTbody(element) { var previousSibling = element.previousSibling; return ( element.nodeName === "TBODY" && (!previousSibling || (previousSibling.nodeName === "THEAD" && /^\s*$/i.test(previousSibling.textContent))) ); } function cell(content: string, node: Node, index: number | null = null) { if (index === null) index = indexOf.call(node.parentNode?.childNodes, node); var prefix = " "; if (index === 0) prefix = "| "; let filteredContent = content.trim().replace(/\n\r/g, "<br>").replace(/\n/g, "<br>"); filteredContent = filteredContent.replace(/\|+/g, "\\|"); while (filteredContent.length < 3) filteredContent += " "; if (node) filteredContent = handleColSpan(filteredContent, node, " "); return prefix + filteredContent + " |"; } function nodeContainsTable(node) { if (!node.childNodes) return false; for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeName === "TABLE") return true; if (nodeContainsTable(child)) return true; } return false; } const nodeContains = (node: Node, types: string | string[]) => { if (!node.childNodes) return false; for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (types === "code" && isCodeBlock(child as HTMLElement)) return true; if (types.includes(child.nodeName)) return true; if (nodeContains(child, types)) return true; } return false; }; const tableShouldBeHtml = (tableNode) => { const possibleTags = ["UL", "OL", "H1", "H2", "H3", "H4", "H5", "H6", "HR", "BLOCKQUOTE"]; // In general we should leave as HTML tables that include other tables. The // exception is with the Web Clipper when we import a web page with a layout // that's made of HTML tables. In that case we have this logic of removing the // outer table and keeping only the inner ones. For the Rich Text editor // however we always want to keep nested tables. possibleTags.push("TABLE"); return nodeContains(tableNode, "code") || nodeContains(tableNode, possibleTags); }; // Various conditions under which a table should be skipped - i.e. each cell // will be rendered one after the other as if they were paragraphs. function tableShouldBeSkipped(tableNode) { const cached = tableShouldBeSkippedCache_.get(tableNode); if (cached !== undefined) return cached; const result = tableShouldBeSkipped_(tableNode); tableShouldBeSkippedCache_.set(tableNode, result); return result; } function tableShouldBeSkipped_(tableNode) { if (!tableNode) return true; if (!tableNode.rows) return true; if (tableNode.rows.length === 1 && tableNode.rows[0].childNodes.length <= 1) return true; // Table with only one cell if (nodeContainsTable(tableNode)) return true; return false; } function nodeParentTable(node) { let parent = node.parentNode; while (parent.nodeName !== "TABLE") { parent = parent.parentNode; if (!parent) return null; } return parent; } function handleColSpan(content, node, emptyChar) { const colspan = node.getAttribute("colspan") || 1; for (let i = 1; i < colspan; i++) { content += " | " + emptyChar.repeat(3); } return content; } function tableColCount(node) { let maxColCount = 0; for (let i = 0; i < node.rows.length; i++) { const row = node.rows[i]; const colCount = row.childNodes.length; if (colCount > maxColCount) maxColCount = colCount; } return maxColCount; } export default function tables(turndownService: TurndownService) { turndownService.keep(function (node) { if (node.nodeName === "TABLE" && tableShouldBeHtml(node)) return true; return false; }); for (var key in rules) turndownService.addRule(key, rules[key]); } ================================================ FILE: api/src/utils/scrape/plugins/taskListItems.ts ================================================ // Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js import TurndownService from "@joplin/turndown"; export default function strikethrough(turndownService: TurndownService) { turndownService.addRule("strikethrough", { filter: ["del", "s", "strike"] as unknown as (keyof HTMLElementTagNameMap)[], replacement: function (content) { return "~~" + content + "~~"; }, }); } ================================================ FILE: api/src/utils/scrape/plugins/utilities.ts ================================================ // Adapted from https://github.com/laurent22/joplin/blob/dev/packages/turndown-plugin-gfm/src/tables.js import css, { CssDeclarationAST, CssFontFaceAST } from "@adobe/css-tools"; export function isCodeBlockSpecialCase1(node: Node) { const parent = node.parentNode; if (!parent) return false; return ( (parent as HTMLElement).classList && (parent as HTMLElement).classList.contains("code") && (parent as HTMLElement).nodeName === "TD" && (node as HTMLElement).nodeName === "PRE" ); } export function isCodeBlockSpecialCase2(node: Node) { if (node.nodeName !== "PRE") return false; const style = (node as HTMLElement).getAttribute("style"); if (!style) return false; const o = css.parse("pre {" + style + "}"); if (!o.stylesheet.rules.length) return; const fontFamily = (o.stylesheet.rules[0] as CssFontFaceAST).declarations.find( (d) => (d as CssDeclarationAST).property.toLowerCase() === "font-family", ); if (!fontFamily || !(fontFamily as CssDeclarationAST).value) return false; const isMonospace = (fontFamily as CssDeclarationAST).value .split(",") .map((e) => e.trim().toLowerCase()) .indexOf("monospace") >= 0; return isMonospace; } export function isCodeBlock(node: Node) { if (isCodeBlockSpecialCase1(node) || isCodeBlockSpecialCase2(node)) return true; return ( (node as HTMLElement).nodeName === "PRE" && (node as HTMLElement).firstChild && (node as HTMLElement).firstChild?.nodeName === "CODE" ); } ================================================ FILE: api/src/utils/scrape/readability.ts ================================================ import { Defuddle } from "defuddle/node"; export const getDefuddleContent = async (htmlString: string) => { const defuddle = await Defuddle(htmlString, undefined, { debug: false, markdown: false, }); return defuddle; }; ================================================ FILE: api/src/utils/scrape/safeGoTo.ts ================================================ import { Page, HTTPResponse } from "puppeteer-core"; /** * Navigates to a URL and ignores net::ERR_ABORTED if the main-frame response is a PDF. * Returns { response, isPdf, pdfResponse }. * * - response: the normal Puppeteer Response from page.goto (null if it aborted on a PDF) * - isPdf: boolean indicating if the main-frame response was a PDF * - pdfResponse: the Response for the PDF (so you can buffer() it, if desired) */ export async function safeGoto(page: Page, url: string, options = {}) { let pdfResponse: HTTPResponse | null = null; const onResponse = (res: HTTPResponse) => { // Only consider main-frame document navigations const req = res.request(); const isMainFrameDoc = req.resourceType() === "document" && req.frame() === page.mainFrame(); if (!isMainFrameDoc) return; const ct = (res.headers()["content-type"] || "").toLowerCase(); console.log("content-type", ct); if (ct.includes("application/pdf")) { pdfResponse = res; } }; page.on("response", onResponse); try { const resp = await page.goto(url, options); return { response: resp, isPdf: !!pdfResponse, pdfResponse }; } catch (err: any) { const message = String((err && err.message) || ""); // If we detected a PDF and Chromium aborted the navigation, swallow it if (pdfResponse && message.includes("net::ERR_ABORTED")) { return { response: null, isPdf: true, pdfResponse }; } throw err; } finally { page.off("response", onResponse); } } ================================================ FILE: api/src/utils/scrape/transformHtml.ts ================================================ import { JSDOM } from "jsdom"; export const transformHtml = (htmlContent: string, baseUrl?: string): string => { const dom = new JSDOM(htmlContent); const document = dom.window.document; optimizeImages(document); if (baseUrl) { normalizeUrls(document, baseUrl); } return document.documentElement.outerHTML; }; const optimizeImages = (document: Document) => { const imagesWithSrcset = document.querySelectorAll("img[srcset]"); imagesWithSrcset.forEach((img) => { const element = img as HTMLImageElement; const srcsetValue = element.getAttribute("srcset"); if (!srcsetValue) return; const imageSources = srcsetValue.split(",").map((entry) => { const parts = entry.trim().split(" "); return { url: parts[0], size: parseInt((parts[1] ?? "1x").slice(0, -1), 10), isPixelDensity: (parts[1] ?? "").endsWith("x"), }; }); const currentSrc = element.getAttribute("src"); if (imageSources.every((source) => source.isPixelDensity) && currentSrc) { imageSources.push({ url: currentSrc, size: 1, isPixelDensity: true, }); } imageSources.sort((a, b) => b.size - a.size); const bestSource = imageSources[0]; if (bestSource) { element.setAttribute("src", bestSource.url); element.removeAttribute("srcset"); } }); }; const normalizeUrls = (document: Document, baseUrl: string) => { try { const urlBase = new URL(baseUrl); const processElements = (selector: string, attribute: string) => { const elements = document.querySelectorAll(selector); elements.forEach((element) => { try { const currentValue = element.getAttribute(attribute); if (currentValue) { element.setAttribute(attribute, new URL(currentValue, urlBase).href); } } catch {} }); }; processElements("img[src]", "src"); processElements("a[href]", "href"); processElements("link[href]", "href"); processElements("video[src]", "src"); processElements("audio[src]", "src"); processElements("source[src]", "src"); } catch {} }; ================================================ FILE: api/src/utils/size.ts ================================================ export const KB = 1000; export const MB = 1000 * KB; // Most proxies use MB, not MiB ================================================ FILE: api/src/utils/text.ts ================================================ export function titleCase(input: string): string { if (!input || typeof input !== "string") { return ""; } return input.charAt(0).toUpperCase() + input.slice(1); } ================================================ FILE: api/src/utils/url.ts ================================================ import { env } from "../env.js"; /** * Returns the appropriate protocol based on the protocol type and HTTPS setting * @param protocolType 'http' or 'ws' - base protocol type * @returns The protocol string with or without 's' suffix based on env.USE_SSL */ function getProtocol(protocolType: "http" | "ws"): string { return env.USE_SSL ? `${protocolType}s` : protocolType; } /** * Returns the base URL for the server, handling DOMAIN vs HOST:PORT appropriately * @param protocolType 'http' or 'ws' - determines the protocol prefix * @returns Formatted base URL with appropriate protocol and trailing slash */ export function getBaseUrl(protocolType: "http" | "ws" = "http"): string { const baseUrl = env.DOMAIN ?? `${env.HOST}:${env.PORT}`; const protocol = getProtocol(protocolType); return `${protocol}://${baseUrl}/`; } /** * Returns a fully qualified URL with the given path * @param path The path to append to the base URL * @param protocolType 'http' or 'ws' - determines the protocol prefix * @returns Formatted URL with appropriate protocol */ export function getUrl(path: string, protocolType: "http" | "ws" = "http"): string { const base = getBaseUrl(protocolType); // Handle paths that might already have a leading slash const formattedPath = path.startsWith("/") ? path.substring(1) : path; return `${base}${formattedPath}`; } /** * Normalizes a URL by adding https:// protocol if missing * @param url The URL to normalize * @returns The normalized URL with proper protocol, or null if invalid */ export function normalizeUrl(url: string): string | null { if (!url || typeof url !== "string") { return null; } const trimmedUrl = url.trim(); if (!trimmedUrl) { return null; } if (trimmedUrl.startsWith("http://") || trimmedUrl.startsWith("https://")) { return trimmedUrl; } const normalizedUrl = `https://${trimmedUrl}`; try { new URL(normalizedUrl); return normalizedUrl; } catch { return null; } } ================================================ FILE: api/tsconfig.json ================================================ { "compilerOptions": { "esModuleInterop": true, "allowSyntheticDefaultImports": true, "module": "Node16", "target": "ESNext", "moduleResolution": "Node16", "skipLibCheck": true, "sourceMap": true, "outDir": "build", "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strict": true, "declaration": true, "declarationMap": true, "experimentalDecorators": true, "emitDecoratorMetadata": true }, "include": ["src"], "files": [ "./src/types/fastify.d.ts", "src/services/cdp/plugins/pptr-extensions.d.ts", "src/types/turndown.d.ts" ] } ================================================ FILE: api/tsconfig.test.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "baseUrl": ".", "noEmit": true }, "include": ["**/*.ts"] } ================================================ FILE: commitlint.config.cjs ================================================ module.exports = { extends: ['@commitlint/config-conventional'], }; ================================================ FILE: docker-compose.dev.yml ================================================ services: api: build: context: . dockerfile: ./api/Dockerfile args: NODE_VERSION: 22.13.0 ports: - "3000:3000" - "9223:9223" environment: - DOMAIN=${DOMAIN:-localhost:3000} - CDP_DOMAIN=${CDP_DOMAIN:-localhost:9223} volumes: - ./.cache:/app/.cache networks: - steel-network ui: build: context: . dockerfile: ./ui/Dockerfile ports: - "5173:80" environment: - API_URL=${API_URL:-http://api:3000} depends_on: - api networks: - steel-network networks: steel-network: name: steel-network driver: bridge ================================================ FILE: docker-compose.yml ================================================ services: api: image: ghcr.io/steel-dev/steel-browser-api:latest ports: - "3000:3000" - "9223:9223" volumes: - logs:/data/logs - exports:/tmp/steel-browser-exports - ./.cache:/app/.cache environment: - DOMAIN=${DOMAIN:-localhost:3000} - CDP_DOMAIN=${CDP_DOMAIN:-localhost:9223} - LOG_STORAGE_ENABLED=true - LOG_STORAGE_PATH=/data/logs/browser-logs.duckdb networks: - steel-network ui: image: ghcr.io/steel-dev/steel-browser-ui:latest ports: - "5173:80" environment: - API_URL=${API_URL:-http://api:3000} depends_on: - api networks: - steel-network networks: steel-network: name: steel-network driver: bridge volumes: logs: driver: local exports: driver: local ================================================ FILE: docs/ARCHITECTURE.md ================================================ # Steel Browser Architecture This document provides a comprehensive overview of Steel Browser's architecture, design decisions, and how the various components work together. ## 🏗️ High-Level Architecture Steel Browser follows a modular, plugin-based architecture designed for extensibility and maintainability: ``` ┌─────────────────────────────────────────────────────────────┐ │ Steel Browser │ ├─────────────────────────────────────────────────────────────┤ │ Frontend (React UI) │ Backend (Fastify API) │ │ ├── Session Management │ ├── CDP Service │ │ ├── Real-time Viewing │ ├── Session Management │ │ ├── DevTools Integration │ ├── File Storage │ │ └── Configuration UI │ └── Plugin System │ ├─────────────────────────────────────────────────────────────┤ │ Chrome/Chromium Browser │ │ ├── Chrome DevTools Protocol (CDP) │ │ ├── Browser Extensions │ │ └── Page Contexts │ └─────────────────────────────────────────────────────────────┘ ``` ## 🔧 Core Components ### 1. CDP Service (`api/src/services/cdp/cdp.service.ts`) The Chrome DevTools Protocol (CDP) Service is the heart of Steel Browser, managing all browser interactions: **Responsibilities:** - Browser lifecycle management (launch, close, restart) - Page creation and navigation - WebSocket proxy for CDP connections - Plugin system coordination - Session state management - Context isolation and fingerprinting **Key Features:** ```typescript class CDPService extends EventEmitter { // Browser management async launch(options?: BrowserLauncherOptions): Promise<Browser> async shutdown(): Promise<void> async refreshPrimaryPage(): Promise<void> // Plugin system registerPlugin(plugin: BasePlugin): void unregisterPlugin(pluginName: string): boolean // Page management async createPage(): Promise<Page> async getPages(): Promise<Page[]> } ``` ### 2. Plugin System Steel Browser's plugin architecture allows for extensible functionality without modifying core code. #### Base Plugin (`api/src/services/cdp/plugins/core/base-plugin.ts`) ```typescript abstract class BasePlugin { // Lifecycle hooks async onBrowserLaunch(browser: Browser): Promise<void> async onPageCreated(page: Page): Promise<void> async onPageNavigate(page: Page): Promise<void> async onPageUnload(page: Page): Promise<void> async onBrowserClose(browser: Browser): Promise<void> async onBeforePageClose(page: Page): Promise<void> async onShutdown(): Promise<void> } ``` #### Plugin Manager (`api/src/services/cdp/plugins/core/plugin-manager.ts`) Coordinates plugin lifecycle and ensures error isolation: - **Registration**: Manages plugin registration and dependency injection - **Event Distribution**: Notifies all plugins of browser events - **Error Handling**: Isolates plugin errors to prevent system crashes - **Lifecycle Management**: Coordinates plugin startup and shutdown ### 3. Session Management (`api/src/services/session.service.ts`) Manages browser sessions with isolated contexts: **Features:** - Session creation with custom configurations - Context isolation (cookies, localStorage, sessionStorage) - Resource cleanup and garbage collection - Session persistence and restoration - Concurrent session management ```typescript interface SessionConfig { proxy?: ProxyConfig; userAgent?: string; viewport?: { width: number; height: number }; extensions?: string[]; fingerprint?: FingerprintOptions; } ``` ### 4. File Storage Service (`api/src/services/file.service.ts`) Handles file operations with session-scoped storage: - **Upload Management**: Handles multipart file uploads - **Download Coordination**: Manages browser downloads - **Storage Isolation**: Session-scoped file storage - **Cleanup**: Automatic file cleanup on session end ## 🔌 Plugin Architecture Deep Dive ### Plugin Lifecycle 1. **Registration**: Plugins register with the PluginManager 2. **Initialization**: Service dependency injection 3. **Event Handling**: Respond to browser lifecycle events 4. **Cleanup**: Graceful shutdown and resource cleanup ### Event Flow ``` Browser Launch → Plugin.onBrowserLaunch() ↓ Page Created → Plugin.onPageCreated() ↓ Page Navigate → Plugin.onPageNavigate() ↓ Page Unload → Plugin.onPageUnload() ↓ Page Close → Plugin.onBeforePageClose() ↓ Browser Close → Plugin.onBrowserClose() ↓ System Shutdown → Plugin.onShutdown() ``` ### Example Plugin Implementation ```typescript import { BasePlugin, PluginOptions } from '@steel-browser/api/cdp-plugin'; import { Browser, Page } from 'puppeteer-core'; export class AdBlockPlugin extends BasePlugin { private blockedDomains: Set<string>; constructor(options: PluginOptions & { blockedDomains?: string[] }) { super({ name: 'ad-blocker', ...options }); this.blockedDomains = new Set(options.blockedDomains || []); } async onPageCreated(page: Page): Promise<void> { await page.setRequestInterception(true); page.on('request', (request) => { const url = new URL(request.url()); if (this.blockedDomains.has(url.hostname)) { request.abort(); } else { request.continue(); } }); } } ``` ## 🌐 API Architecture ### Fastify Plugin System Steel Browser uses Fastify's plugin architecture for modular API design: ```typescript // Main plugin registration await fastify.register(steelBrowserPlugin, { fileStorage: { maxSizePerSession: 100 * MB } }); // Individual plugins await fastify.register(browserInstancePlugin); await fastify.register(sessionPlugin); await fastify.register(fileStoragePlugin); ``` ### Route Organization Routes are organized by functionality: - **Actions** (`/v1/*`): Browser automation actions (scrape, screenshot, PDF) - **Sessions** (`/v1/sessions/*`): Session management - **CDP** (`/v1/cdp/*`): Direct CDP access - **Files** (`/v1/files/*`): File upload/download - **Selenium** (`/selenium/*`): Selenium WebDriver compatibility ### Schema Validation All API endpoints use Zod schemas for validation: ```typescript const ScrapeRequestSchema = z.object({ url: z.string().url().optional(), delay: z.number().optional(), format: z.array(z.enum(['html','cleaned_html','markdown','readability'])).optional(), screenshot: z.boolean().optional(), pdf: z.boolean().optional(), }); ``` ## 🎨 Frontend Architecture ### React Component Structure ``` src/ ├── components/ # Reusable UI components │ ├── ui/ # Base UI components (buttons, inputs) │ ├── badges/ # Status badges │ ├── icons/ # Icon components │ └── sessions/ # Session-specific components ├── containers/ # Page-level containers ├── contexts/ # React contexts for state management ├── hooks/ # Custom React hooks └── steel-client/ # Auto-generated API client ``` ### State Management - **React Query**: Server state management and caching - **React Context**: Global application state - **Local State**: Component-specific state with hooks ### Real-time Updates WebSocket connections provide real-time updates: ```typescript // Session monitoring const { data: sessions } = useQuery({ queryKey: ['sessions'], queryFn: () => steelClient.sessions.getSessions(), refetchInterval: 1000 // Real-time updates }); ``` ## 🔒 Security Architecture ### Input Validation - **API Level**: Zod schema validation for all inputs - **Browser Level**: Content Security Policy (CSP) headers - **File Level**: File type validation and size limits ### Context Isolation Each session runs in an isolated browser context: ```typescript const context = await browser.createIncognitoBrowserContext(); context.setDefaultNavigationTimeout(30000); context.setDefaultTimeout(30000); ``` ### Resource Limits - **Memory**: Browser process memory limits - **CPU**: Process CPU throttling - **Storage**: Session-scoped file storage limits - **Network**: Request rate limiting and proxy support ## 📊 Performance Considerations ### Browser Resource Management - **Process Isolation**: Each session in separate browser context - **Memory Cleanup**: Automatic page and context cleanup - **Connection Pooling**: Reuse CDP connections where possible ### Caching Strategy - **Static Assets**: Long-term caching for UI assets - **API Responses**: Short-term caching for session data - **Browser Cache**: Configurable per-session browser caching ### Scaling Considerations Current architecture supports: - **Vertical Scaling**: Multi-core CPU utilization - **Session Concurrency**: Multiple simultaneous sessions - **Resource Monitoring**: Memory and CPU usage tracking Future scaling options: - **Horizontal Scaling**: Multiple Steel instances - **Load Balancing**: Session distribution - **Distributed Storage**: Shared file storage ## 🧪 Testing Architecture ### Test Structure (Planned) ``` tests/ ├── unit/ # Unit tests for individual components ├── integration/ # API endpoint integration tests ├── e2e/ # End-to-end browser automation tests └── performance/ # Load and performance tests ``` ### Testing Strategy - **Unit Tests**: Core services and utilities - **Integration Tests**: API endpoints and database interactions - **E2E Tests**: Full browser automation workflows - **Performance Tests**: Load testing and benchmarking ## 🔧 Configuration Management ### Environment Variables Configuration through environment variables: ```typescript const envSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']), HOST: z.string().default('0.0.0.0'), PORT: z.string().default('3000'), CHROME_EXECUTABLE_PATH: z.string().optional(), CHROME_HEADLESS: z.boolean().default(true), // ... more configuration options }); ``` ### Runtime Configuration - **Browser Options**: Per-session browser configuration - **Plugin Configuration**: Dynamic plugin options - **Feature Flags**: Runtime feature toggling ## 🚀 Deployment Architecture ### Containerization Multi-stage Docker builds for optimization: ```dockerfile # Build stage FROM node:22-slim AS build # ... build steps # Production stage FROM node:22-slim AS production # ... production setup ``` ### Service Dependencies - **Chrome/Chromium**: Browser engine - **Node.js**: Runtime environment - **Nginx**: Reverse proxy (in containers) - **File System**: Session storage ## 🔄 Development Workflow ### Hot Reloading Development environment supports hot reloading: ```bash npm run dev # Starts both API and UI with hot reload ``` ### Debug Configuration Built-in debugging support: ```bash # API debugging node --inspect ./api/build/index.js # Enable verbose logging ENABLE_VERBOSE_LOGGING=true npm run dev -w api ``` ## 📈 Monitoring and Observability ### Logging Structured logging with Pino: ```typescript fastify.log.info({ sessionId, action: 'page_created', url: page.url() }, 'New page created'); ``` ### Metrics (Planned) - **Session Metrics**: Creation, duration, success rates - **Performance Metrics**: Response times, resource usage - **Error Tracking**: Error rates and categorization ### Health Checks Built-in health check endpoints: ```typescript // Basic health check GET /health // Detailed readiness check GET /ready ``` --- This architecture provides a solid foundation for browser automation while maintaining flexibility for future enhancements and scaling requirements. ================================================ FILE: docs/DEVELOPMENT_SETUP.md ================================================ # Development Setup Guide This guide provides comprehensive instructions for setting up a Steel Browser development environment. ## 🎯 Prerequisites ### System Requirements - **Operating System**: Linux, macOS, or Windows (with WSL2 recommended) - **RAM**: Minimum 8GB, recommended 16GB+ - **Storage**: At least 10GB free space - **Network**: Stable internet connection for dependencies ### Required Software #### 1. Node.js (Version 22+) **Linux/macOS:** ```bash # Using Node Version Manager (recommended) curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash source ~/.bashrc nvm install 22 nvm use 22 # Or using package manager # Ubuntu/Debian sudo apt update sudo apt install nodejs npm # macOS with Homebrew brew install node@22 ``` **Windows:** ```powershell # Using Chocolatey choco install nodejs --version=22.0.0 # Or download from https://nodejs.org/ ``` #### 2. Git **Linux:** ```bash sudo apt install git # Ubuntu/Debian sudo yum install git # CentOS/RHEL ``` **macOS:** ```bash brew install git # or use Xcode Command Line Tools xcode-select --install ``` **Windows:** ```powershell choco install git # or download from https://git-scm.com/ ``` #### 3. Chrome/Chromium Browser **Linux:** ```bash # Ubuntu/Debian wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt update sudo apt install google-chrome-stable # Or Chromium sudo apt install chromium-browser ``` **macOS:** ```bash brew install --cask google-chrome # or download from https://www.google.com/chrome/ ``` **Windows:** ```powershell choco install googlechrome # or download from https://www.google.com/chrome/ ``` #### 4. Docker (Optional but Recommended) **Linux:** ```bash # Ubuntu/Debian sudo apt update sudo apt install docker.io docker-compose sudo usermod -aG docker $USER newgrp docker # CentOS/RHEL sudo yum install docker docker-compose sudo systemctl start docker sudo systemctl enable docker sudo usermod -aG docker $USER ``` **macOS:** ```bash brew install --cask docker # or download Docker Desktop from https://www.docker.com/products/docker-desktop ``` **Windows:** ```powershell choco install docker-desktop # or download Docker Desktop from https://www.docker.com/products/docker-desktop ``` ## 🚀 Quick Setup ### 1. Clone the Repository ```bash # Fork the repository first on GitHub, then clone your fork git clone https://github.com/YOUR_USERNAME/steel-browser.git cd steel-browser # Add upstream remote git remote add upstream https://github.com/steel-dev/steel-browser.git ``` ### 2. Install Dependencies ```bash # Install all workspace dependencies npm install # Verify installation npm list --depth=0 ``` ### 3. Build the Project ```bash # Build all workspaces npm run build # Or build individually npm run build -w api npm run build -w ui ``` ### 4. Start Development Environment ```bash # Start both API and UI in development mode npm run dev # This will start: # - API server on http://localhost:3000 # - UI server on http://localhost:5173 ``` ### 5. Verify Setup ```bash # Test API curl http://localhost:3000/v1/health # Test UI open http://localhost:5173 # Test REPL cd repl npm start ``` ## 🐳 Docker Development Setup ### 1. Using Docker Compose (Recommended) ```bash # Start development environment docker-compose -f docker-compose.dev.yml up --build # Or in detached mode docker-compose -f docker-compose.dev.yml up -d --build # View logs docker-compose -f docker-compose.dev.yml logs -f ``` ### 2. Individual Container Setup ```bash # Build API container docker build -t steel-browser-api -f ./api/Dockerfile . # Build UI container docker build -t steel-browser-ui -f ./ui/Dockerfile . # Run API container docker run -p 3000:3000 -p 9223:9223 steel-browser-api # Run UI container docker run -p 5173:80 steel-browser-ui ``` ## 🔧 Development Configuration ### Environment Variables Create a `.env` file in the root directory: ```bash # .env NODE_ENV=development HOST=0.0.0.0 PORT=3000 CDP_REDIRECT_PORT=9223 # Chrome Configuration CHROME_HEADLESS=false # Set to true for headless mode CHROME_EXECUTABLE_PATH=/usr/bin/google-chrome # Adjust path as needed ENABLE_CDP_LOGGING=true ENABLE_VERBOSE_LOGGING=true # Development Features DEBUG_CHROME_PROCESS=false LOG_CUSTOM_EMIT_EVENTS=true # UI Configuration API_URL=http://localhost:3000 ``` ### Chrome Configuration #### Finding Chrome Executable **Linux:** ```bash which google-chrome which chromium-browser # Common paths: # /usr/bin/google-chrome # /usr/bin/chromium-browser ``` **macOS:** ```bash # Common path: # /Applications/Google Chrome.app/Contents/MacOS/Google Chrome ``` **Windows:** ```powershell # Common paths: # C:\Program Files\Google\Chrome\Application\chrome.exe # C:\Program Files (x86)\Google\Chrome\Application\chrome.exe ``` #### Custom Chrome Arguments ```bash # Add custom Chrome arguments export CHROME_ARGS="--disable-web-security --disable-features=VizDisplayCompositor" # Filter out problematic arguments export FILTER_CHROME_ARGS="--disable-dev-shm-usage" ``` ## 🛠️ IDE Setup ### Visual Studio Code #### Recommended Extensions ```bash # Install VS Code extensions code --install-extension ms-typescript.typescript code --install-extension esbenp.prettier-vscode code --install-extension bradlc.vscode-tailwindcss code --install-extension ms-vscode.vscode-typescript-next code --install-extension ms-vscode.vscode-eslint ``` #### Settings Configuration Create `.vscode/settings.json`: ```json { "typescript.preferences.importModuleSpecifier": "relative", "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "typescript.preferences.includePackageJsonAutoImports": "on", "files.exclude": { "**/node_modules": true, "**/build": true, "**/dist": true, "**/.cache": true } } ``` #### Debug Configuration Create `.vscode/launch.json`: ```json { "version": "0.2.0", "configurations": [ { "name": "Debug API", "type": "node", "request": "launch", "program": "${workspaceFolder}/api/build/index.js", "outFiles": ["${workspaceFolder}/api/build/**/*.js"], "env": { "NODE_ENV": "development", "ENABLE_VERBOSE_LOGGING": "true", "CHROME_HEADLESS": "false" }, "console": "integratedTerminal", "restart": true, "runtimeArgs": ["--inspect"] }, { "name": "Debug Tests", "type": "node", "request": "launch", "program": "${workspaceFolder}/node_modules/.bin/vitest", "args": ["run"], "cwd": "${workspaceFolder}", "console": "integratedTerminal" } ] } ``` ### WebStorm/IntelliJ IDEA #### Run Configurations 1. **API Development** - Type: Node.js - JavaScript file: `api/build/index.js` - Environment variables: `NODE_ENV=development;ENABLE_VERBOSE_LOGGING=true` 2. **UI Development** - Type: npm - Command: `run` - Scripts: `dev` - Package.json: `ui/package.json` ## 🧪 Testing Setup ### Unit Testing ```bash # Install testing dependencies (if not already installed) npm install -D vitest @vitest/ui jsdom # Run tests npm test # Run tests in watch mode npm test -- --watch # Run tests with UI npm test -- --ui ``` ### Integration Testing ```bash # Start test environment NODE_ENV=test npm run dev -w api # Run integration tests npm run test:integration ``` ### End-to-End Testing ```bash # Install E2E testing framework npm install -D playwright @playwright/test # Run E2E tests npx playwright test # Run E2E tests with UI npx playwright test --ui ``` ## 📊 Development Tools ### Code Quality ```bash # Format code npm run pretty -w api npm run lint -w ui # Check types npm run type-check -w api npm run type-check -w ui # Pre-commit hooks (automatically set up with Husky) npm run prepare ``` ### Performance Monitoring ```bash # Start with performance profiling node --prof ./api/build/index.js # Generate performance report node --prof-process isolate-*.log > performance.txt # Memory usage monitoring node --inspect --expose-gc ./api/build/index.js ``` ### Database/Storage Tools ```bash # View session storage ls -la ./db/data/ # View file storage ls -la ./files/ # Clear storage rm -rf ./db/data/* rm -rf ./files/* ``` ## 🔄 Development Workflow ### Daily Development ```bash # 1. Update your fork git fetch upstream git checkout main git merge upstream/main # 2. Create feature branch git checkout -b feature/my-new-feature # 3. Start development environment npm run dev # 4. Make changes and test # ... develop your feature ... # 5. Run quality checks npm run pretty -w api npm run lint -w ui npm run build # 6. Commit changes git add . git commit -m "feat: add new feature" # 7. Push and create PR git push origin feature/my-new-feature ``` ### Hot Reloading The development environment supports hot reloading: - **API**: Uses `tsx watch` for automatic TypeScript compilation and restart - **UI**: Uses Vite's hot module replacement (HMR) - **Extensions**: Require manual rebuild (`npm run prepare:recorder -w api`) ### Debugging Browser Issues ```bash # Run Chrome in non-headless mode export CHROME_HEADLESS=false npm run dev -w api # Enable Chrome debugging export DEBUG_CHROME_PROCESS=true # Connect to Chrome DevTools # Open http://localhost:9223 in your browser ``` ## 🚨 Troubleshooting Development Setup ### Common Issues #### 1. Node.js Version Mismatch ```bash # Check current version node --version # Switch to correct version nvm use 22 # Set as default nvm alias default 22 ``` #### 2. Permission Issues (Linux/macOS) ```bash # Fix npm permissions sudo chown -R $(whoami) ~/.npm sudo chown -R $(whoami) /usr/local/lib/node_modules # Fix project permissions sudo chown -R $(whoami) ./node_modules ``` #### 3. Chrome Not Found ```bash # Find Chrome installation which google-chrome which chromium-browser # Set Chrome path export CHROME_EXECUTABLE_PATH=/path/to/chrome ``` #### 4. Port Conflicts ```bash # Check what's using the port lsof -i :3000 lsof -i :5173 # Kill conflicting processes kill -9 $(lsof -t -i:3000) # Use different ports export PORT=3001 ``` #### 5. Memory Issues ```bash # Increase Node.js memory limit export NODE_OPTIONS="--max-old-space-size=4096" # Monitor memory usage htop ``` ### Getting Help If you encounter issues: 1. Check the [Troubleshooting Guide](./TROUBLESHOOTING.md) 2. Search existing GitHub issues 3. Ask in our Discord community 4. Create a detailed bug report ## 📚 Next Steps After setting up your development environment: 1. **Read the [Architecture Guide](./ARCHITECTURE.md)** to understand the system design 2. **Explore the [Plugin Development Guide](./PLUGIN_DEVELOPMENT.md)** to create extensions 3. **Check out the [Contributing Guide](../CONTRIBUTING.md)** for contribution guidelines 4. **Browse the [Steel Cookbook](https://github.com/steel-dev/steel-cookbook)** for usage examples ## 🎉 You're Ready! Your Steel Browser development environment is now set up and ready for development. Happy coding! 🚀 --- **Need help?** Join our [Discord community](https://discord.gg/steel-dev) for real-time support! ================================================ FILE: docs/PLUGIN_DEVELOPMENT.md ================================================ # Plugin Development Guide This guide walks you through developing custom plugins for Steel Browser's extensible architecture. ## 🚀 Quick Start ### Creating Your First Plugin ```typescript import { BasePlugin, PluginOptions } from '@steel-browser/api/cdp-plugin'; import { Browser, Page } from 'puppeteer-core'; export class HelloWorldPlugin extends BasePlugin { constructor(options: PluginOptions) { super({ name: 'hello-world', ...options }); } async onBrowserLaunch(browser: Browser): Promise<void> { console.log('Hello from the browser launch event!'); } async onPageCreated(page: Page): Promise<void> { console.log(`New page created: ${page.url()}`); } } // Usage const plugin = new HelloWorldPlugin({}); cdpService.registerPlugin(plugin); ``` ## 🏗️ Plugin Architecture ### Base Plugin Class All plugins extend the `BasePlugin` abstract class: ```typescript abstract class BasePlugin { public name: string; protected options: PluginOptions; protected cdpService: CDPService | null; constructor(options: PluginOptions); public setService(service: CDPService): void; // Lifecycle hooks (all optional) public async onBrowserLaunch(browser: Browser): Promise<void> {} public async onPageCreated(page: Page): Promise<void> {} public async onPageNavigate(page: Page): Promise<void> {} public async onPageUnload(page: Page): Promise<void> {} public async onBrowserClose(browser: Browser): Promise<void> {} public async onBeforePageClose(page: Page): Promise<void> {} public async onShutdown(): Promise<void> {} } ``` ### Plugin Options ```typescript interface PluginOptions { name: string; [key: string]: any; // Additional plugin-specific options } ``` ## 🔄 Lifecycle Events ### Event Order ``` 1. onBrowserLaunch - Browser process starts 2. onPageCreated - New page/tab created 3. onPageNavigate - Page navigates to URL 4. onPageUnload - Page unloads/navigates away 5. onBeforePageClose - Before page closes 6. onBrowserClose - Browser process closes 7. onShutdown - Plugin cleanup ``` ### Event Details #### onBrowserLaunch(browser: Browser) - Called when the browser process starts - Use for browser-level configuration - Access to the Browser instance #### onPageCreated(page: Page) - Called when a new page/tab is created - Perfect for page-level setup (request interception, etc.) - Access to the Page instance #### onPageNavigate(page: Page) - Called before page navigation - Use for URL-based logic or navigation tracking #### onPageUnload(page: Page) - Called when page unloads or navigates away - Cleanup page-specific resources #### onBeforePageClose(page: Page) - Called before a page closes - Last chance for page cleanup #### onBrowserClose(browser: Browser) - Called when browser process closes - Browser-level cleanup #### onShutdown() - Called during plugin shutdown - Final cleanup opportunity ## 📝 Plugin Examples ### 1. Request Logger Plugin ```typescript export class RequestLoggerPlugin extends BasePlugin { private logFile: string; constructor(options: PluginOptions & { logFile?: string }) { super({ name: 'request-logger', ...options }); this.logFile = options.logFile || 'requests.log'; } async onPageCreated(page: Page): Promise<void> { await page.setRequestInterception(true); page.on('request', (request) => { const logEntry = { timestamp: new Date().toISOString(), method: request.method(), url: request.url(), headers: request.headers() }; // Log to file or console console.log('Request:', logEntry); request.continue(); }); } } ``` ### 2. Ad Blocker Plugin ```typescript export class AdBlockerPlugin extends BasePlugin { private blockedDomains: Set<string>; private blockedCount: number = 0; constructor(options: PluginOptions & { blockedDomains?: string[] }) { super({ name: 'ad-blocker', ...options }); this.blockedDomains = new Set(options.blockedDomains || [ 'doubleclick.net', 'googleadservices.com', 'googlesyndication.com' ]); } async onPageCreated(page: Page): Promise<void> { await page.setRequestInterception(true); page.on('request', (request) => { const url = new URL(request.url()); if (this.blockedDomains.has(url.hostname)) { this.blockedCount++; console.log(`Blocked ad request: ${url.hostname}`); request.abort(); } else { request.continue(); } }); } async onShutdown(): Promise<void> { console.log(`Ad Blocker: Blocked ${this.blockedCount} requests`); } } ``` ### 3. Screenshot Plugin ```typescript export class ScreenshotPlugin extends BasePlugin { private screenshotDir: string; constructor(options: PluginOptions & { screenshotDir?: string }) { super({ name: 'screenshot', ...options }); this.screenshotDir = options.screenshotDir || './screenshots'; } async onPageNavigate(page: Page): Promise<void> { // Take screenshot after navigation setTimeout(async () => { try { const url = new URL(page.url()); const filename = `${url.hostname}-${Date.now()}.png`; const filepath = path.join(this.screenshotDir, filename); await page.screenshot({ path: filepath, fullPage: true }); console.log(`Screenshot saved: ${filepath}`); } catch (error) { console.error('Screenshot failed:', error); } }, 2000); } } ``` ### 4. Performance Monitor Plugin ```typescript export class PerformancePlugin extends BasePlugin { private metrics: Map<string, any> = new Map(); constructor(options: PluginOptions) { super({ name: 'performance-monitor', ...options }); } async onPageCreated(page: Page): Promise<void> { // Enable performance monitoring await page.coverage.startJSCoverage(); await page.coverage.startCSSCoverage(); page.on('load', async () => { const performanceMetrics = await page.evaluate(() => { const perfData = performance.getEntriesByType('navigation')[0]; return { domContentLoaded: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart, loadComplete: perfData.loadEventEnd - perfData.loadEventStart, firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0 }; }); this.metrics.set(page.url(), performanceMetrics); console.log(`Performance metrics for ${page.url()}:`, performanceMetrics); }); } async onShutdown(): Promise<void> { console.log('Performance Summary:', Object.fromEntries(this.metrics)); } } ``` ## 🔧 Advanced Plugin Development ### Accessing CDP Service ```typescript export class AdvancedPlugin extends BasePlugin { async onBrowserLaunch(browser: Browser): Promise<void> { // Access the CDP service if (this.cdpService) { // Get all pages const pages = await this.cdpService.getPages(); // Access primary page const primaryPage = this.cdpService.primaryPage; // Register hooks this.cdpService.registerLaunchHook(async (config) => { console.log('Browser launching with config:', config); }); } } } ``` ### Plugin Configuration ```typescript interface MyPluginOptions extends PluginOptions { apiKey?: string; endpoint?: string; retries?: number; timeout?: number; } export class ConfigurablePlugin extends BasePlugin { private config: MyPluginOptions; constructor(options: MyPluginOptions) { super(options); this.config = { apiKey: options.apiKey || process.env.API_KEY, endpoint: options.endpoint || 'https://api.example.com', retries: options.retries || 3, timeout: options.timeout || 5000, ...options }; } } ``` ### Error Handling ```typescript export class RobustPlugin extends BasePlugin { async onPageCreated(page: Page): Promise<void> { try { // Plugin logic here await this.setupPageInterception(page); } catch (error) { console.error(`Error in ${this.name} plugin:`, error); // Plugin errors are isolated by the PluginManager // but you should handle them gracefully } } private async setupPageInterception(page: Page): Promise<void> { // Implementation with proper error handling } } ``` ## 🧪 Testing Plugins ### Unit Testing ```typescript // plugin.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { MyPlugin } from './my-plugin'; describe('MyPlugin', () => { let plugin: MyPlugin; beforeEach(() => { plugin = new MyPlugin({ name: 'test-plugin' }); }); it('should initialize with correct name', () => { expect(plugin.name).toBe('test-plugin'); }); it('should handle browser launch', async () => { const mockBrowser = {} as any; // Mock browser object await expect(plugin.onBrowserLaunch(mockBrowser)).resolves.not.toThrow(); }); }); ``` ### Integration Testing ```typescript // integration.test.ts import { CDPService } from '@steel-browser/api'; import { MyPlugin } from './my-plugin'; describe('Plugin Integration', () => { let cdpService: CDPService; let plugin: MyPlugin; beforeEach(async () => { cdpService = new CDPService({}, console); plugin = new MyPlugin({ name: 'test-plugin' }); cdpService.registerPlugin(plugin); }); afterEach(async () => { await cdpService.shutdown(); }); it('should work with CDP service', async () => { await cdpService.launch(); // Test plugin behavior }); }); ``` ## 📦 Plugin Distribution ### NPM Package Structure ``` my-steel-plugin/ ├── src/ │ ├── index.ts │ └── plugin.ts ├── dist/ ├── package.json ├── README.md └── tsconfig.json ``` ### package.json Example ```json { "name": "steel-plugin-example", "version": "1.0.0", "description": "Example plugin for Steel Browser", "main": "dist/index.js", "types": "dist/index.d.ts", "keywords": ["steel-browser", "plugin", "automation"], "peerDependencies": { "@steel-browser/api": "^1.0.0", "puppeteer-core": "^23.0.0" }, "files": ["dist/**/*"] } ``` ### TypeScript Configuration ```json { "extends": "@steel-browser/api/tsconfig.json", "compilerOptions": { "outDir": "./dist", "declaration": true, "declarationMap": true }, "include": ["src/**/*"] } ``` ## 🌟 Best Practices ### 1. Error Handling - Always wrap plugin logic in try-catch blocks - Log errors with context information - Don't let plugin errors crash the system ### 2. Resource Management - Clean up resources in shutdown hooks - Remove event listeners when done - Close files and connections properly ### 3. Performance - Avoid blocking operations in event handlers - Use async/await properly - Consider memory usage for long-running plugins ### 4. Configuration - Provide sensible defaults - Support environment variables - Validate configuration options ### 5. Testing - Write unit tests for plugin logic - Test with real browser instances - Mock external dependencies ### 6. Documentation - Document plugin options and usage - Provide examples - Include troubleshooting guides ## 🔍 Debugging Plugins ### Enable Debug Logging ```bash # Enable verbose logging ENABLE_VERBOSE_LOGGING=true npm run dev -w api # Enable CDP logging ENABLE_CDP_LOGGING=true npm run dev -w api ``` ### Debug in VS Code ```json // .vscode/launch.json { "configurations": [ { "name": "Debug Steel with Plugin", "type": "node", "request": "launch", "program": "${workspaceFolder}/api/build/index.js", "env": { "NODE_ENV": "development", "ENABLE_VERBOSE_LOGGING": "true" } } ] } ``` ### Plugin Debug Helper ```typescript export class DebugPlugin extends BasePlugin { private debug: boolean; constructor(options: PluginOptions & { debug?: boolean }) { super(options); this.debug = options.debug || false; } private log(...args: any[]): void { if (this.debug) { console.log(`[${this.name}]`, ...args); } } async onPageCreated(page: Page): Promise<void> { this.log('Page created:', page.url()); // Plugin logic... } } ``` ## 📚 Plugin Registry (Future) We're planning a plugin registry where you can: - Publish plugins for community use - Discover existing plugins - Rate and review plugins - Automatic plugin updates Stay tuned for updates on this feature! ## 🤝 Contributing Plugins To contribute a plugin to the Steel Browser ecosystem: 1. Create a well-documented plugin 2. Add comprehensive tests 3. Submit to the community registry 4. Engage with users for feedback --- Happy plugin development! 🚀 ================================================ FILE: docs/README.md ================================================ # Steel Browser Documentation Welcome to the Steel Browser documentation! This directory contains comprehensive guides and references to help you understand, use, and contribute to Steel Browser. ## 📚 Documentation Overview ### Getting Started - **[Development Setup Guide](DEVELOPMENT_SETUP.md)** - Complete setup instructions for development environment - **[Contributing Guide](../CONTRIBUTING.md)** - How to contribute to the project - **[Troubleshooting Guide](TROUBLESHOOTING.md)** - Common issues and solutions ### Architecture & Design - **[Architecture Overview](ARCHITECTURE.md)** - System design and component relationships - **[Plugin Development Guide](PLUGIN_DEVELOPMENT.md)** - Creating custom plugins ### API Reference - **[API Documentation](http://localhost:3000/documentation)** - Interactive API reference (when running locally) - **[OpenAPI Schema](../api/openapi/schemas.json)** - Machine-readable API specification ## 🚀 Quick Links ### For New Contributors 1. Start with the [Contributing Guide](../CONTRIBUTING.md) 2. Set up your development environment using the [Development Setup Guide](DEVELOPMENT_SETUP.md) 3. Read the [Architecture Overview](ARCHITECTURE.md) to understand the system 4. Check out issues labeled [`good first issue`](https://github.com/steel-dev/steel-browser/labels/good%20first%20issue) ### For Plugin Developers 1. Read the [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) 2. Study the [Architecture Overview](ARCHITECTURE.md) for system understanding 3. Browse existing plugins in `api/src/services/cdp/plugins/` 4. Join our [Discord](https://discord.gg/steel-dev) for plugin development discussions ### For Users 1. Check the main [README](../README.md) for basic usage 2. Browse the [Steel Cookbook](https://github.com/steel-dev/steel-cookbook) for examples 3. Use the [Troubleshooting Guide](TROUBLESHOOTING.md) if you encounter issues 4. Visit the [API Documentation](http://localhost:3000/documentation) for detailed API reference ## 🛠️ Documentation Structure ``` docs/ ├── README.md # This file - documentation overview ├── ARCHITECTURE.md # System architecture and design ├── DEVELOPMENT_SETUP.md # Development environment setup ├── PLUGIN_DEVELOPMENT.md # Plugin creation guide └── TROUBLESHOOTING.md # Common issues and solutions ``` ## 📖 External Resources ### Official Resources - **[Steel Browser Repository](https://github.com/steel-dev/steel-browser)** - Main repository - **[Steel Cookbook](https://github.com/steel-dev/steel-cookbook)** - Usage examples and recipes - **[Discord Community](https://discord.gg/steel-dev)** - Real-time support and discussions - **[Official Documentation](https://docs.steel.dev/)** - Comprehensive online docs ### Learning Resources - **[Puppeteer Documentation](https://pptr.dev/)** - Browser automation library - **[Fastify Documentation](https://www.fastify.io/)** - Web framework used in API - **[React Documentation](https://react.dev/)** - Frontend framework - **[TypeScript Handbook](https://www.typescriptlang.org/docs/)** - TypeScript language guide ## 🤝 Contributing to Documentation We welcome contributions to improve our documentation! Here's how you can help: ### Reporting Documentation Issues - **Missing Information**: If you can't find what you're looking for - **Outdated Content**: If documentation doesn't match current behavior - **Unclear Instructions**: If steps are confusing or incomplete - **Broken Links**: If links don't work or point to wrong resources ### Improving Documentation 1. **Fork the repository** and create a feature branch 2. **Make your changes** to the relevant documentation files 3. **Test your changes** by following the instructions you've written 4. **Submit a pull request** with a clear description of improvements ### Documentation Standards - **Use clear, concise language** that's accessible to all skill levels - **Include code examples** where applicable - **Add screenshots** for UI-related documentation - **Keep examples up-to-date** with current API and features - **Cross-reference related sections** to help users navigate ## 🔍 Finding What You Need ### By Use Case **I want to...** - **Use Steel Browser** → Start with the main [README](../README.md) - **Contribute code** → Read the [Contributing Guide](../CONTRIBUTING.md) - **Set up development** → Follow the [Development Setup Guide](DEVELOPMENT_SETUP.md) - **Create a plugin** → Study the [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) - **Understand the system** → Read the [Architecture Overview](ARCHITECTURE.md) - **Fix an issue** → Check the [Troubleshooting Guide](TROUBLESHOOTING.md) ### By Component **I'm working with...** - **API/Backend** → [Architecture](ARCHITECTURE.md) + [API Docs](http://localhost:3000/documentation) - **Frontend/UI** → [Architecture](ARCHITECTURE.md) + UI source code - **Plugins** → [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) - **Docker** → [Development Setup](DEVELOPMENT_SETUP.md) + [Troubleshooting](TROUBLESHOOTING.md) ## 📋 Documentation Roadmap ### Planned Additions - **Deployment Guide** - Production deployment instructions - **Security Guide** - Security best practices and configuration - **Performance Guide** - Optimization and scaling recommendations - **Testing Guide** - Comprehensive testing strategies - **Migration Guide** - Upgrading between versions - **FAQ** - Frequently asked questions ### Community Contributions Needed - **Platform-specific guides** (Windows, macOS, Linux variations) - **Integration examples** with popular tools and frameworks - **Video tutorials** for complex setup procedures - **Translated documentation** for non-English speakers - **Real-world use case studies** ## 💡 Getting Help If you can't find what you're looking for in the documentation: 1. **Search existing issues** on GitHub 2. **Ask in Discord** for real-time help 3. **Create a documentation issue** describing what's missing 4. **Check the [Steel Cookbook](https://github.com/steel-dev/steel-cookbook)** for practical examples ## 🎯 Documentation Goals Our documentation aims to be: - **Comprehensive** - Covering all aspects of Steel Browser - **Accessible** - Understandable by users of all skill levels - **Up-to-date** - Reflecting the current state of the project - **Practical** - Including real-world examples and use cases - **Community-driven** - Improved through user feedback and contributions --- **Happy learning and building with Steel Browser!** 🚀 *Last updated: [Current Date] - If you notice outdated information, please let us know!* ================================================ FILE: docs/TROUBLESHOOTING.md ================================================ # Troubleshooting Guide This guide helps you diagnose and resolve common issues with Steel Browser. ## 🚀 Quick Diagnostics ### Health Check Commands ```bash # Check if services are running curl http://localhost:3000/v1/health # Check API documentation curl http://localhost:3000/documentation # Test basic functionality cd repl && npm start ``` ### Environment Verification ```bash # Check Node.js version (should be 22+) node --version # Check npm version npm --version # Check Chrome/Chromium google-chrome --version # or chromium --version # Check Docker (if using containers) docker --version docker-compose --version ``` ## 🔧 Common Issues ### 1. Browser Launch Failures #### Symptoms - "Failed to launch browser" errors - Chrome executable not found - Permission denied errors #### Solutions **Chrome Not Found:** ```bash # Set Chrome executable path export CHROME_EXECUTABLE_PATH=/usr/bin/google-chrome # or for macOS export CHROME_EXECUTABLE_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" # or for Windows set CHROME_EXECUTABLE_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe" ``` **Permission Issues (Linux):** ```bash # Add user to necessary groups sudo usermod -a -G audio,video $USER # Install required dependencies sudo apt-get update sudo apt-get install -y \ libnss3-dev \ libatk-bridge2.0-dev \ libdrm-dev \ libxcomposite-dev \ libxdamage-dev \ libxrandr-dev \ libgbm-dev \ libxss-dev \ libasound2-dev ``` **Headless Mode Issues:** ```bash # Disable headless mode for debugging export CHROME_HEADLESS=false # Or run with virtual display export DISPLAY=:99 Xvfb :99 -screen 0 1024x768x24 & ``` ### 2. Port Conflicts #### Symptoms - "Port already in use" errors - Cannot connect to API - Services fail to start #### Solutions ```bash # Check what's using the ports lsof -i :3000 # API port lsof -i :5173 # UI port lsof -i :9223 # CDP port # Kill processes using the ports kill -9 $(lsof -t -i:3000) # Use different ports export PORT=3001 export CDP_REDIRECT_PORT=9224 ``` ### 3. Memory Issues #### Symptoms - Browser crashes - "Out of memory" errors - Slow performance #### Solutions ```bash # Increase Node.js memory limit export NODE_OPTIONS="--max-old-space-size=4096" # Monitor memory usage docker stats # if using Docker htop # system monitor # Reduce concurrent sessions # Limit browser instances in your code ``` ### 4. Network/Proxy Issues #### Symptoms - Cannot reach external websites - Proxy authentication failures - SSL/TLS errors #### Solutions ```bash # Set proxy configuration export PROXY_URL="http://proxy.company.com:8080" export PROXY_URL="http://username:password@proxy.company.com:8080" # Disable SSL verification (development only) export NODE_TLS_REJECT_UNAUTHORIZED=0 # Check network connectivity curl -I https://google.com ``` ### 5. File Permission Issues #### Symptoms - Cannot write files - Session storage errors - Extension loading failures #### Solutions ```bash # Fix file permissions chmod -R 755 ./files chmod -R 755 ./.cache # Check disk space df -h # Create required directories mkdir -p ./files mkdir -p ./.cache mkdir -p ./api/extensions/recorder/dist ``` ### 6. Docker Issues #### Symptoms - Container fails to start - Cannot access services - Build failures #### Solutions ```bash # Clean Docker cache docker system prune -a # Rebuild without cache docker-compose build --no-cache # Check container logs docker-compose logs api docker-compose logs ui # Fix volume permissions sudo chown -R $USER:$USER ./.cache ``` ## 🐛 Debugging Techniques ### 1. Enable Debug Logging ```bash # Enable all debug logging export NODE_ENV=development export ENABLE_VERBOSE_LOGGING=true export ENABLE_CDP_LOGGING=true export LOG_CUSTOM_EMIT_EVENTS=true # Start with debug logging npm run dev -w api ``` ### 2. Chrome DevTools Debugging ```bash # Start API with inspector node --inspect ./api/build/index.js # Or with specific port node --inspect=0.0.0.0:9229 ./api/build/index.js # Then open Chrome and go to: # chrome://inspect ``` ### 3. Browser Debugging ```bash # Run Chrome in non-headless mode export CHROME_HEADLESS=false # Enable Chrome debugging export DEBUG_CHROME_PROCESS=true # Connect to browser DevTools # Open http://localhost:9223 in your browser ``` ### 4. Network Debugging ```bash # Monitor network requests export ENABLE_CDP_LOGGING=true # Use network debugging tools tcpdump -i any port 3000 wireshark # GUI network analyzer ``` ### 5. Performance Debugging ```bash # Enable performance monitoring node --prof ./api/build/index.js # Generate performance report node --prof-process isolate-*.log > performance.txt # Memory profiling node --inspect --expose-gc ./api/build/index.js ``` ## 📊 Log Analysis ### Understanding Log Levels ``` ERROR - Critical issues requiring immediate attention WARN - Potential issues that might cause problems INFO - General information about system operation DEBUG - Detailed information for troubleshooting TRACE - Very detailed execution information ``` ### Common Log Patterns **Successful Session Creation:** ``` INFO: Session created successfully {sessionId: "abc123"} INFO: Browser launched {pid: 12345} INFO: Primary page created {url: "about:blank"} ``` **Connection Issues:** ``` ERROR: Failed to connect to Chrome {error: "ECONNREFUSED"} WARN: Retrying browser launch {attempt: 2} ``` **Memory Warnings:** ``` WARN: High memory usage detected {usage: "85%"} INFO: Garbage collection triggered ``` ## 🔍 Diagnostic Commands ### System Information ```bash # Get system info uname -a # System information free -h # Memory usage df -h # Disk usage ps aux | grep chrome # Chrome processes ps aux | grep node # Node processes ``` ### Steel Browser Specific ```bash # Check API health curl -s http://localhost:3000/v1/health | jq # List active sessions curl -s http://localhost:3000/v1/sessions | jq # Get session details curl -s http://localhost:3000/v1/sessions/SESSION_ID | jq # Check file service ls -la ./files/ # Check cache ls -la ./.cache/ ``` ### Docker Diagnostics ```bash # Container status docker-compose ps # Container logs docker-compose logs --tail=50 api docker-compose logs --tail=50 ui # Container resource usage docker stats # Network information docker network ls docker network inspect steel-network ``` ## 🚨 Error Codes ### HTTP Status Codes - **400 Bad Request**: Invalid request parameters - **404 Not Found**: Session or resource not found - **408 Request Timeout**: Operation timed out - **409 Conflict**: Resource conflict (e.g., session already exists) - **500 Internal Server Error**: Server-side error - **503 Service Unavailable**: Browser not available ### Custom Error Codes - **BROWSER_LAUNCH_FAILED**: Cannot start browser process - **SESSION_NOT_FOUND**: Session ID doesn't exist - **PAGE_LOAD_TIMEOUT**: Page failed to load within timeout - **CHROME_EXECUTABLE_NOT_FOUND**: Chrome binary not found - **INSUFFICIENT_MEMORY**: Not enough memory to start browser ## 🛠️ Recovery Procedures ### 1. Restart Services ```bash # Graceful restart npm run dev # Ctrl+C then restart # Force restart pkill -f "node.*steel" npm run dev # Docker restart docker-compose restart ``` ### 2. Clear Cache and Temp Files ```bash # Clear application cache rm -rf ./.cache/* rm -rf ./files/* # Clear npm cache npm cache clean --force # Clear Docker cache docker system prune -f ``` ### 3. Reset to Clean State ```bash # Stop all services docker-compose down # Remove volumes docker-compose down -v # Rebuild everything docker-compose build --no-cache docker-compose up ``` ### 4. Database/Storage Recovery ```bash # Clear session storage rm -rf ./db/data/* # Reset file storage rm -rf ./files/* mkdir -p ./files # Fix permissions chmod -R 755 ./files chmod -R 755 ./.cache ``` ## 📞 Getting Help ### Before Asking for Help 1. **Check this troubleshooting guide** 2. **Search existing GitHub issues** 3. **Enable debug logging and collect logs** 4. **Try the basic recovery procedures** 5. **Prepare a minimal reproduction case** ### Information to Include When reporting issues, include: ```bash # System information uname -a node --version npm --version google-chrome --version # Steel Browser logs (last 50 lines) tail -50 steel-browser.log # Configuration env | grep -E "(CHROME|PORT|HOST|NODE)" # Steps to reproduce 1. Start Steel Browser 2. Create session with X configuration 3. Navigate to Y URL 4. Error occurs ``` ### Support Channels - **GitHub Issues**: Bug reports and feature requests - **Discord**: Real-time community support - **Documentation**: Comprehensive guides and API reference - **Stack Overflow**: Tag questions with `steel-browser` ### Creating Good Bug Reports ```markdown ## Bug Description Clear description of what's wrong ## Steps to Reproduce 1. Step one 2. Step two 3. Error occurs ## Expected Behavior What should happen ## Actual Behavior What actually happens ## Environment - OS: Ubuntu 20.04 - Node: v22.0.0 - Steel Browser: v1.0.0 - Chrome: 91.0.4472.124 ## Logs ``` Include relevant log output ``` ## Additional Context Any other relevant information ``` ## 🔧 Advanced Troubleshooting ### Core Dumps ```bash # Enable core dumps ulimit -c unlimited # Analyze core dump gdb node core.12345 ``` ### Memory Leaks ```bash # Use heap profiler node --inspect --expose-gc ./api/build/index.js # Take heap snapshots kill -USR2 <node_pid> ``` ### Network Issues ```bash # Test network connectivity ping google.com nslookup google.com traceroute google.com # Check proxy settings echo $HTTP_PROXY echo $HTTPS_PROXY echo $NO_PROXY ``` --- Still having issues? Don't hesitate to reach out to our community for help! 🤝 ================================================ FILE: nginx.conf ================================================ events { worker_connections 1024; } http { server { listen 9223; location / { proxy_pass http://127.0.0.1:9222; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } } } ================================================ FILE: package.json ================================================ { "name": "steel-browser", "version": "0.5.1", "private": true, "license": "Apache-2.0", "author": "", "type": "module", "workspaces": [ "api", "ui", "repl" ], "scripts": { "build": "npm run build -w api -w ui", "dev": "concurrently \"npm run dev -w api\" \"npm run dev -w ui\"", "prepare": "husky" }, "devDependencies": { "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", "@types/archiver": "^6.0.3", "@types/turndown": "^5.0.5", "concurrently": "^8.2.0", "tsx": "^4.19.2", "typescript": "^5.7.3" }, "engines": { "node": ">=22" }, "dependencies": { "@joplin/turndown-plugin-gfm": "^1.0.62", "@types/lodash-es": "^4.17.12", "fastify": "^5.2.1", "husky": "^9.1.7", "lodash-es": "^4.17.21" }, "overrides": { "tar-fs": ">=3.1.1" } } ================================================ FILE: render.yaml ================================================ services: - type: web runtime: docker name: steel-browser dockerfilePath: ./Dockerfile healthCheckPath: /v1/health envVars: - key: NODE_ENV value: production - key: DOMAIN value: "<change me to your render domain>" - key: USE_SSL value: "true" disk: name: cache mountPath: /app/.cache sizeGB: 1 ================================================ FILE: repl/README.md ================================================ # Steel REPL This package provides a simple REPL to interact with the browser instance you've created using the API. The API exposes a WebSocket endpoint, allowing you to connect to the browser using Chrome DevTools Protocol (CDP) and use Puppeteer as usual. ## Quick Start 1. Ensure you have **Steel Browser** running, either via Docker or locally. 2. Run `npm start` to execute the script. 3. Modify `src/script.ts` as needed and rerun `npm start` to see your changes. > Note: You might need to update the WebSocket endpoint in `src/script.ts` if your services isn't exposed on your network For more details, refer to [Steel Browser Documentation](https://docs.steel.dev/). ================================================ FILE: repl/package.json ================================================ { "name": "@steel-browser/repl", "private": true, "sideEffects": false, "type": "module", "scripts": { "start": "tsx ./src/script.ts" }, "dependencies": { "puppeteer-core": "^24.2.0" }, "devDependencies": { "tsx": "*" }, "engines": { "node": ">=22.0.0" }, "overrides": { "tar-fs": ">=3.1.1" } } ================================================ FILE: repl/src/script.ts ================================================ import puppeteer from "puppeteer-core"; async function run() { // WebSocket endpoint to connect Browser using Chrome DevTools Protocol (CDP) const wsEndpoint = "ws://0.0.0.0:3000"; const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint }); try { const page = await browser.newPage(); // Navigate to a website and log the title await page.goto("https://steel.dev"); console.log(`Page title: ${await page.title()}`); } finally { // Cleanup: close all pages and disconnect browser await Promise.all((await browser.pages()).map((p) => p.close())); await browser.disconnect(); } } run().catch(console.error); ================================================ FILE: ui/.dockerignore ================================================ node_modules/ .env.local .env ================================================ FILE: ui/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended", ], ignorePatterns: ["dist", ".eslintrc.cjs"], parser: "@typescript-eslint/parser", plugins: ["react-refresh"], rules: { "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, ], "react-hooks/exhaustive-deps": "off", "@typescript-eslint/no-explicit-any": "off", }, }; ================================================ FILE: ui/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? .env.local ================================================ FILE: ui/Dockerfile ================================================ ARG NODE_VERSION=22.13.0 FROM node:${NODE_VERSION} AS base WORKDIR /app LABEL org.opencontainers.image.source="https://github.com/steel-dev/steel-browser" # Copy package.json and package-lock.json first COPY --link package.json package-lock.json ./ COPY --link ui/ ./ui/ # Install the npm packages directly in the Docker container's working directory RUN npm ci --include=dev -w ui --ignore-scripts # Build the application RUN npm run build -w ui # Prune dev dependencies RUN npm prune --omit=dev -w ui FROM nginx:alpine COPY --from=base /app/ui/dist /usr/share/nginx/html COPY --from=base /app/ui/nginx.conf.template /etc/nginx/nginx.conf.template COPY --chmod=755 --from=base /app/ui/entrypoint.sh /docker-entrypoint.sh EXPOSE 80 ENTRYPOINT ["/docker-entrypoint.sh"] ================================================ FILE: ui/README.md ================================================ # React + TypeScript + Vite This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Currently, two official plugins are available: - [@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 - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh ## Expanding the ESLint configuration If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - Configure the top-level `parserOptions` property like this: ```js parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: ['./tsconfig.json', './tsconfig.node.json'], tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), }, ``` - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` - 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 ================================================ FILE: ui/components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": false, "tsx": true, "tailwind": { "config": "tailwind.config.js", "css": "src/index.css", "baseColor": "zinc", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" } } ================================================ FILE: ui/entrypoint.sh ================================================ #!/bin/sh set -e log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" } substitute_env_vars() { log "Substituting environment variables in nginx config template..." sed -e "s|__API_URL__|${API_URL}|g" /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf } main() { substitute_env_vars log "Starting nginx..." exec nginx -g 'daemon off;' } main "$@" ================================================ FILE: ui/index.html ================================================ <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/png" href="/icon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta property="og:title" content="Steel API" /> <meta property="og:description" 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." /> <meta property="og:type" content="website" /> <meta property="og:url" content="https://steel.dev" /> <meta name="description" content="Steel is an open-source browser API purpose-built for AI agents, offering browser control, data extraction, and session management capabilities." /> <title>Steel | Open-source Headless Browser API
================================================ FILE: ui/nginx.conf.template ================================================ worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; server { listen 80; location /api/ { proxy_pass __API_URL__/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /ws/ { proxy_pass __API_URL__/; # Required for WebSocket proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; } location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } } } ================================================ FILE: ui/openapi-ts.config.ts ================================================ import { defineConfig } from "@hey-api/openapi-ts"; import dotenv from "dotenv"; dotenv.config({ path: ".env.local" }); export default defineConfig({ client: "@hey-api/client-fetch", input: "../api/openapi/schemas.json", output: { format: "prettier", path: "./src/steel-client", }, types: { dates: "types+transform", enums: "javascript", }, }); ================================================ FILE: ui/package.json ================================================ { "name": "@steel-browser/ui", "private": true, "version": "0.0.0", "type": "module", "scripts": { "start": "serve dist -l 5173", "dev": "vite --host ${HOST:-0.0.0.0}", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 10", "preview": "vite preview", "generate-api": "openapi-ts" }, "dependencies": { "@fontsource/inter": "^5.0.13", "@hey-api/client-fetch": "^0.4.0", "@hookform/resolvers": "^3.9.0", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/themes": "^3.0.3", "@tanstack/react-query": "^4.36.1", "@tanstack/react-table": "^8.20.5", "@vitejs/plugin-react": "^4.3.4", "axios": "^1.12.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "i": "^0.3.7", "jwt-decode": "^3.1.2", "lucide-react": "^0.447.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.53.0", "react-router-dom": "^6.16.0", "react-top-loading-bar": "^2.3.1", "rrweb-player": "^1.0.0-alpha.4", "serve": "^14.0.1", "styled-components": "^6.0.8", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "ua-parser-js": "^1.0.39", "usehooks-ts": "^3.1.0", "zod": "^3.23.8" }, "devDependencies": { "@hey-api/openapi-ts": "^0.53.6", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.18", "@types/node": "^20.8.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-syntax-highlighter": "^15.5.8", "@types/styled-components": "^5.1.28", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react-swc": "^3.3.2", "autoprefixer": "^10.4.20", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "postcss": "^8.4.47", "tailwindcss": "^3.4.13", "typescript": "^5.7.3", "vite": "^6.3.5" }, "overrides": { "tar-fs": ">=3.1.1" } } ================================================ FILE: ui/postcss.config.js ================================================ export default { plugins: { tailwindcss: {}, autoprefixer: {}, }, } ================================================ FILE: ui/src/App.tsx ================================================ import "@fontsource/inter"; import "@radix-ui/themes/styles.css"; import RootLayout from "@/root-layout"; import { client } from "@/steel-client"; import { env } from "@/env"; client.setConfig({ baseUrl: env.VITE_API_URL, }); function App() { return ; } export default App; ================================================ FILE: ui/src/components/badges/proxy-badge.tsx ================================================ import { CopyIcon } from "@radix-ui/react-icons"; import { Badge } from "@/components/ui/badge"; import { copyText } from "@/utils/toasts"; export function ProxyBadge({ proxy }: { proxy: string }) { return ( {proxy} copyText(proxy, "Proxy IP")} /> ); } ================================================ FILE: ui/src/components/badges/user-agent-badge.tsx ================================================ import { DesktopIcon, CopyIcon } from "@radix-ui/react-icons"; import { Badge } from "@/components/ui/badge"; import { ChromeIcon } from "@/components/icons/ChromeIcon"; import { copyText } from "@/utils/toasts"; import UAParser from "ua-parser-js"; export function UserAgentBadge({ userAgent }: { userAgent: string }) { const parser = new UAParser(userAgent); return ( {" "} {parser.getDevice().type || "Desktop"} {" "} {`${parser.getBrowser().name} (v${parser.getBrowser().version})`} copyText(userAgent, "User Agent")} /> ); } ================================================ FILE: ui/src/components/badges/websocket-url-badge.tsx ================================================ import { CopyIcon } from "@radix-ui/react-icons"; import { Badge } from "@/components/ui/badge"; import { copyText } from "@/utils/toasts"; export function WebsocketUrlBadge({ url }: { url: string }) { return ( {url} copyText(url, "Websocket URL")} /> ); } ================================================ FILE: ui/src/components/header/header.tsx ================================================ import { ChevronRightIcon } from "@radix-ui/react-icons"; import { Badge } from "@/components/ui/badge"; import { GlowingGreenDot } from "@/components/icons/GlowingGreenDot"; import { useSessionsContext } from "@/hooks/use-sessions-context"; import { SteelIcon } from "../icons/SessionIcon"; export const Header = () => { const { pathname } = window.location; const currentSessionId = pathname.includes("sessions") && pathname.split("/").pop() !== "sessions" ? pathname.split("/").pop() : null; const { useSession } = useSessionsContext(); const { data: session, isLoading } = useSession(currentSessionId!); return (
{currentSessionId ? ( <> Session ) : ( <> Session )}
{currentSessionId && ( <>
#{currentSessionId.split("-")[0]} {!isLoading && session?.status === "live" && ( Live )}
)}
); }; ================================================ FILE: ui/src/components/header/index.tsx ================================================ export { Header } from "./header"; ================================================ FILE: ui/src/components/icons/ChromeIcon.tsx ================================================ import { IconProps } from "@/types/props"; export function ChromeIcon({ width = 12, height = 12, color = "currentColor", }: IconProps) { return ( ); } ================================================ FILE: ui/src/components/icons/DeleteIcon.tsx ================================================ import { IconProps } from "@/types/props"; export function DeleteIcon({ width = 28, height = 28 }: IconProps) { return ( ); } ================================================ FILE: ui/src/components/icons/GlobeIcon.tsx ================================================ export const GlobeIcon = () => { return ( ); }; ================================================ FILE: ui/src/components/icons/GlowingGreenDot.tsx ================================================ export const GlowingGreenDot = () => { return ( ); }; ================================================ FILE: ui/src/components/icons/KeyIcon.tsx ================================================ import { IconProps } from "@/types/props"; export function KeyIcon({ width = 12, height = 12, color = "currentColor", }: IconProps) { return ( ); } ================================================ FILE: ui/src/components/icons/LoadingSpinner.tsx ================================================ import { cn } from "@/lib/utils"; export interface LoadingSpinnerProps { className?: string; } export const LoadingSpinner = ({ className }: LoadingSpinnerProps) => { return ( ); }; ================================================ FILE: ui/src/components/icons/NinjaIcon.tsx ================================================ import { IconProps } from "@/types/props"; export function NinjaIcon({ color = "#A1A1AA" }: IconProps) { return ( ); } ================================================ FILE: ui/src/components/icons/SessionIcon.tsx ================================================ export const SteelIcon = () => { return ( ); }; ================================================ FILE: ui/src/components/icons/SettingsIcon.tsx ================================================ export const SettingsIcon = () => { return ( ); }; ================================================ FILE: ui/src/components/illustrations/command-line.tsx ================================================ export function CommandLine() { return ( ); } ================================================ FILE: ui/src/components/illustrations/globe.tsx ================================================ export function Globe() { return ( ); } ================================================ FILE: ui/src/components/loading/Loading.styles.tsx ================================================ import styled from "styled-components"; export const Container = styled.div` display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; `; ================================================ FILE: ui/src/components/loading/Loading.tsx ================================================ import * as Styled from "./Loading.styles"; export function Loading() { return LOADING...; } ================================================ FILE: ui/src/components/loading/index.tsx ================================================ export { Loading } from "./Loading"; ================================================ FILE: ui/src/components/sessions/release-session-dialog.tsx ================================================ import { DialogHeader, DialogFooter, Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { useSessionsContext } from "@/hooks/use-sessions-context"; import { useEffect, useState } from "react"; export const ReleaseSessionDialog = ({ children, id, }: { id: string; children: React.ReactNode; }) => { const [open, setOpen] = useState(false); const { useReleaseSessionMutation } = useSessionsContext(); const { mutate: releaseSession, isLoading, isSuccess, } = useReleaseSessionMutation(); useEffect(() => { if (isSuccess) { setOpen(false); } }, [isSuccess]); return ( {children} Are you absolutely sure? This action cannot be undone. This will release your session and delete all your session data. {!isLoading && ( )} ); }; ================================================ FILE: ui/src/components/sessions/session-console/index.tsx ================================================ import { Tabs, TabsTrigger, TabsList } from "@/components/ui/tabs"; import { useState } from "react"; import SessionDetails from "./session-details"; import SessionLogs from "./session-logs"; import SessionDevTools from "./session-devtools"; interface SessionConsoleProps { id: string | null; } export default function SessionConsole({ id }: SessionConsoleProps) { const [activeTab, setActiveTab] = useState<"details" | "logs" | "dev-tools">( "details" ); const tabs: { value: "details" | "logs" | "dev-tools"; label: string }[] = [ { value: "details", label: "Details" }, { value: "logs", label: "Logs" }, { value: "dev-tools", label: "Dev Tools" }, ]; return (
{tabs.map((tab) => ( setActiveTab(tab.value)} className={`!bg-transparent !box-shadow-none rounded-none p-4 ${ activeTab === tab.value ? "border-b-2 border-b-[var(--gray-11)]" : "" }`} > {tab.label} ))}
{activeTab === "details" && } {activeTab === "logs" && } {activeTab === "dev-tools" && }
); } ================================================ FILE: ui/src/components/sessions/session-console/session-details.tsx ================================================ import { useSessionsContext } from "@/hooks/use-sessions-context"; import { Skeleton } from "@radix-ui/themes"; import { ReleaseSessionDialog } from "../release-session-dialog"; import { Button } from "@/components/ui/button"; export default function SessionDetails({ id }: { id: string | null }) { const { useSession } = useSessionsContext(); const { data: session, isLoading, isError } = useSession(id!); return (
{isLoading && ( <>
)} {isError &&
Error loading session
} {session && ( <>
ID
{session.id}
Timestamp
{session.createdAt.toLocaleString()}
Duration
{session.duration}
User Agent
{session.userAgent}
Auto-captcha
{session.solveCaptcha?.toString()}
isSelenium
{session.isSelenium?.toString()}
Websocket URL
{session.websocketUrl.slice(0, 30)}
)} {session?.status === "live" && (
)}
); } ================================================ FILE: ui/src/components/sessions/session-console/session-devtools.tsx ================================================ import { env } from "@/env"; import { useEffect, useState } from "react"; export default function SessionDevTools() { const [pageId, setPageId] = useState(null); useEffect(() => { const ws = new WebSocket(`${env.VITE_API_URL}/v1/sessions/pageId`); ws.onmessage = (event) => { setPageId(event.data.pageId); }; return () => { ws.close(); }; }, []); useEffect(() => { if (!pageId) return; const iframe = document.querySelector("iframe"); if (iframe) { iframe.src = iframe.src + ""; } }, [pageId]); return (