diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..80ea62c
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+
+repo_root="$(git rev-parse --show-toplevel)"
+cd "$repo_root"
+
+echo "pre-commit: running lint autofix"
+npm run lint:fix
+
+echo "pre-commit: running formatter"
+npm run format
+
+git add -A -- src
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index b15cc60..95cc355 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -31,10 +31,9 @@ body:
description: Minimal code to reproduce the issue
render: typescript
placeholder: |
- import { request } from 'node-wreq';
+ import { fetch } from 'node-wreq';
- const response = await request({
- url: 'https://example.com',
+ const response = await fetch('https://example.com', {
browser: 'chrome_131'
});
validations:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 49df015..3e8b260 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: π Documentation
- url: https://github.com/will-work-for-meal/node-wreq#readme
+ url: https://github.com/StopMakingThatBigFace/node-wreq#readme
about: Read the documentation and guides
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9f389e3..0c9be86 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,6 +20,25 @@ jobs:
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
build: npm run build:rust -- --target x86_64-unknown-linux-gnu
+ - host: ubuntu-24.04-arm
+ target: aarch64-unknown-linux-gnu
+ build: npm run build:rust -- --target aarch64-unknown-linux-gnu
+ - host: ubuntu-latest
+ target: x86_64-unknown-linux-musl
+ setup: |
+ set -eux
+ TOOLCHAIN_URL="https://github.com/troglobit/misc/releases/download/11-20211120/x86_64-linux-musl-cross.tgz"
+ TOOLCHAIN_ROOT="$HOME/musl-cross"
+ TOOLCHAIN_DIR="$TOOLCHAIN_ROOT/x86_64-linux-musl-cross"
+
+ if [ ! -d "$TOOLCHAIN_DIR" ]; then
+ mkdir -p "$TOOLCHAIN_ROOT"
+ curl -L "$TOOLCHAIN_URL" -o /tmp/x86_64-linux-musl-cross.tgz
+ tar -xzf /tmp/x86_64-linux-musl-cross.tgz -C "$TOOLCHAIN_ROOT"
+ fi
+
+ echo "$TOOLCHAIN_DIR/bin" >> "$GITHUB_PATH"
+ build: CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc CC_x86_64_unknown_linux_musl=x86_64-linux-musl-gcc CXX_x86_64_unknown_linux_musl=x86_64-linux-musl-g++ AR_x86_64_unknown_linux_musl=x86_64-linux-musl-ar RANLIB_x86_64_unknown_linux_musl=x86_64-linux-musl-ranlib npm run build:rust -- --target x86_64-unknown-linux-musl
- host: windows-latest
target: x86_64-pc-windows-msvc
build: npm run build:rust -- --target x86_64-pc-windows-msvc
@@ -40,6 +59,10 @@ jobs:
with:
targets: ${{ matrix.settings.target }}
+ - name: Install target toolchain dependencies
+ if: matrix.settings.setup != ''
+ run: ${{ matrix.settings.setup }}
+
- name: Install dependencies
run: npm install
@@ -54,13 +77,23 @@ jobs:
if-no-files-found: error
publish:
- name: Publish to npm
+ name: Publish platform package - ${{ matrix.settings.target }}
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
permissions:
contents: read
id-token: write
+ strategy:
+ fail-fast: false
+ matrix:
+ settings:
+ - target: x86_64-apple-darwin
+ - target: aarch64-apple-darwin
+ - target: x86_64-unknown-linux-gnu
+ - target: aarch64-unknown-linux-gnu
+ - target: x86_64-unknown-linux-musl
+ - target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
@@ -68,30 +101,108 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: 20
+ node-version: 24
registry-url: 'https://registry.npmjs.org'
+ - name: Determine npm dist-tag
+ id: dist_tag
+ shell: bash
+ run: |
+ if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then
+ echo "value=rc" >> "$GITHUB_OUTPUT"
+ else
+ echo "value=latest" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Determine publish version
+ id: publish_version
+ shell: bash
+ run: |
+ tag="${{ github.event.release.tag_name }}"
+ version="${tag#v}"
+
+ if [[ -z "$version" ]]; then
+ echo "Release tag produced an empty version" >&2
+ exit 1
+ fi
+
+ echo "value=$version" >> "$GITHUB_OUTPUT"
+
- name: Install dependencies
run: npm ci
- - name: Download all artifacts
+ - name: Download artifact
uses: actions/download-artifact@v4
with:
+ name: bindings-${{ matrix.settings.target }}
path: artifacts
- - name: Move artifacts to rust directory
+ - name: Prepare scoped platform package
+ shell: bash
+ env:
+ NODE_WREQ_PUBLISH_VERSION: ${{ steps.publish_version.outputs.value }}
+ run: |
+ BINARY_PATH="$(find artifacts -name '*.node' | head -n 1)"
+ node ./scripts/prepare-platform-package.mjs \
+ --target "${{ matrix.settings.target }}" \
+ --binary "$BINARY_PATH" \
+ --outDir ".release/${{ matrix.settings.target }}"
+
+ - name: Publish scoped platform package
+ run: npm publish ".release/${{ matrix.settings.target }}" --access public --tag "${{ steps.dist_tag.outputs.value }}"
+
+ publish-main:
+ name: Publish main package
+ runs-on: ubuntu-latest
+ needs: publish
+ if: github.event_name == 'release'
+ permissions:
+ contents: read
+ id-token: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Determine npm dist-tag
+ id: dist_tag
shell: bash
run: |
- mkdir -p rust
- # Copy all .node files from artifacts to rust/
- find artifacts -name "*.node" -exec cp {} rust/ \;
- echo "Collected binaries:"
- ls -la rust/*.node
+ if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then
+ echo "value=rc" >> "$GITHUB_OUTPUT"
+ else
+ echo "value=latest" >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Determine publish version
+ id: publish_version
+ shell: bash
+ run: |
+ tag="${{ github.event.release.tag_name }}"
+ version="${tag#v}"
+
+ if [[ -z "$version" ]]; then
+ echo "Release tag produced an empty version" >&2
+ exit 1
+ fi
+
+ echo "value=$version" >> "$GITHUB_OUTPUT"
+
+ - name: Install dependencies
+ run: npm ci
- name: Build TypeScript
run: npm run build:ts
- - name: Publish to npm
- run: npm publish --provenance --access public
+ - name: Prepare main npm package
env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ NODE_WREQ_PUBLISH_VERSION: ${{ steps.publish_version.outputs.value }}
+ run: npm run prepare:publish:main -- .release/main-package
+
+ - name: Publish main package
+ run: npm publish .release/main-package --access public --tag "${{ steps.dist_tag.outputs.value }}"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index da95a25..096079f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -82,5 +82,8 @@ jobs:
- name: Check TypeScript types
run: npx tsc --noEmit
+ - name: Run oxlint
+ run: npm run lint
+
- name: Check code formatting
run: npm run format:check
diff --git a/.gitignore b/.gitignore
index 57a5d16..c9d34bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,9 @@ node_modules/
# Build outputs
dist/
+.release/
+.release-stubs/
+.tmp-release/
rust/target/
rust/node-wreq.node
*.node
@@ -28,4 +31,4 @@ yarn-debug.log*
yarn-error.log*
# Agents
-.claude/
\ No newline at end of file
+.claude/
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 0000000..26d3627
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
+ "sortPackageJson": false,
+ "sortImports": {
+ "sortSideEffects": true,
+ "newlinesBetween": false
+ },
+ "ignorePatterns": [
+ "**/dist/**",
+ "**/node_modules/**",
+ ],
+ "semi": true,
+ "trailingComma": "es5",
+ "singleQuote": true,
+ "bracketSpacing": true,
+ "tabWidth": 2,
+ "printWidth": 100,
+ "endOfLine": "lf"
+}
\ No newline at end of file
diff --git a/.oxlintrc.json b/.oxlintrc.json
new file mode 100644
index 0000000..68d641b
--- /dev/null
+++ b/.oxlintrc.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "jsPlugins": [
+ {
+ "name": "stylistic",
+ "specifier": "@stylistic/eslint-plugin"
+ }
+ ],
+ "rules": {
+ "stylistic/padding-line-between-statements": [
+ "error",
+ {
+ "blankLine": "always",
+ "prev": [
+ "const",
+ "let",
+ "var"
+ ],
+ "next": "*"
+ },
+ {
+ "blankLine": "always",
+ "prev": "*",
+ "next": "return"
+ },
+ {
+ "blankLine": "any",
+ "prev": [
+ "const",
+ "let",
+ "var"
+ ],
+ "next": [
+ "const",
+ "let",
+ "var"
+ ]
+ }
+ ]
+ }
+}
diff --git a/.prettierignore b/.prettierignore
deleted file mode 100644
index e578bea..0000000
--- a/.prettierignore
+++ /dev/null
@@ -1,22 +0,0 @@
-# Dependencies
-node_modules
-package-lock.json
-yarn.lock
-pnpm-lock.yaml
-
-# Build output
-dist
-rust/target
-*.node
-
-# Cache
-.cache
-*.log
-
-# IDE
-.vscode
-.idea
-
-# Misc
-coverage
-.DS_Store
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index 32a2397..0000000
--- a/.prettierrc
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "semi": true,
- "trailingComma": "es5",
- "singleQuote": true,
- "printWidth": 100,
- "tabWidth": 2,
- "useTabs": false,
- "arrowParens": "always",
- "endOfLine": "lf"
-}
diff --git a/LICENSE b/LICENSE
index 4e8bd1e..b88a2d2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 will-work-for-meal
+Copyright (c) 2025 StopMakingThatBigFace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index aa0fb18..e5702c6 100644
--- a/README.md
+++ b/README.md
@@ -1,283 +1,780 @@
# node-wreq
-π High-performance browser fingerprint bypass library using Rust for native TLS/HTTP2 impersonation.
+[](https://npmjs.com/package/node-wreq)
+
+
-> **Node.js wrapper for [wreq](https://github.com/0x676e67/wreq)** β A powerful Rust HTTP client with browser impersonation capabilities.
+`node-wreq` is a thin Node.js wrapper around 0x676e67's [`wreq`](https://github.com/0x676e67/wreq) β
+a Rust HTTP client exposing its full power to JavaScript.
-## β¨ Features
+Use it when you need low-level control over the network layer: TLS configuration,
+transport fingerprinting, browser impersonation, or fine-grained HTTP/WebSocket
+behavior that standard Node.js clients simply don't expose.
-- β‘ **Native Performance** β 50-100x faster than curl-impersonate (no process spawning)
-- π **TLS Fingerprinting** β Perfect JA3/JA4 signatures matching real browsers
-- π **HTTP/2 Fingerprinting** β Authentic SETTINGS frames, PRIORITY, and header ordering
-- π **Multiple Browser Profiles** β Chrome, Firefox, Safari, Edge, Opera, OkHttp (78+ profiles)
-- π **WebSocket Support**
-- π¦ **Zero Dependencies** β Pure Rust with BoringSSL under the hood
-- π **TypeScript Support** β Full type definitions included
-- π‘οΈ **Protection Bypass** β Cloudflare, Akamai, and other anti-bot systems
+> [!TIP]
+> ### why does this exist?
+>
+> Node.js ships with a built-in `https` module, and the ecosystem offers popular clients like `axios`, `got`, and `node-fetch` β but all of them are built on top of OpenSSL via Node's `tls` module, which exposes **no control over low-level TLS handshake parameters**. This makes it fundamentally impossible to emulate real browser network behavior from pure JavaScript.
+>
+> - **HTTP/1 over TLS**
+>
+> Node.js HTTP clients normalize headers to lowercase internally, which is compliant with HTTP/2 semantics but breaks compatibility with some **WAFs** that enforce case-sensitive header validation on **HTTP/1** requests. This wrapper preserves header case exactly as specified, preventing requests from being silently rejected.
+>
+> - **HTTP/2 over TLS**
+>
+> Fingerprints like **JA3**, **JA4**, and **Akamai HTTP/2** are derived from the specifics of the TLS handshake and HTTP/2 SETTINGS frames β cipher suite ordering, TLS extensions, ALPN values, HPACK header compression parameters, and more. Node.js exposes none of these through its `tls` or `http2` APIs. You simply cannot spoof them from JS land, no matter the library. This package solves that at the native layer, giving you fine-grained control over TLS and HTTP/2 extensions to precisely match real browser behavior.
+>
+> - **Device Emulation**
+>
+> Because TLS and HTTP/2 fingerprints evolve slowly relative to browser release cycles, a single fingerprint profile often covers many browser versions. **100+ pre-built browser device profiles** are bundled, so you don't have to figure out the right combination of settings yourself.
-## π§ How It Works
+TLS and HTTP/2 fingerprinting is actively used by major bot protection and WAF providers β including **Cloudflare** Bot Management, **AWS WAF** (Bot Control + CloudFront JA3 headers), **Google Cloud Armor**, **Akamai** (which maintains its own HTTP/2 fingerprint format on top of JA3/JA4), **ServicePipe** (a Russian DDoS protection and WAF provider), and various specialized anti-bot services like **DataDome** and **PerimeterX**. Correctly emulating a browser's TLS handshake and HTTP/2 SETTINGS frames is a hard requirement to get past these layers undetected.
-The library is a Node.js wrapper for **[wreq](https://github.com/0x676e67/wreq)** (Rust HTTP client) with **BoringSSL** (Google's TLS library β the same one used in Chrome) to create requests that are indistinguishable from real browsers at the network level.
+
[**TLS Fingerprinting with JA3 and JA3S**](https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967/)
-### Why It Works
+
[**JA3/JA4 Fingerprint β Cloudflare Bot Solutions**](https://developers.cloudflare.com/bots/additional-configurations/ja3-ja4-fingerprint/)
-Traditional HTTP clients (axios, fetch, curl) have differences in:
-- **TLS handshake signatures** β Different cipher suites and extensions
-- **HTTP/2 frame ordering** β Different SETTINGS and PRIORITY patterns
-- **Header ordering** β Different sequence and values
+
[**TLS Fingerprinting: How It Works & How to Bypass It**](https://www.browserless.io/blog/tls-fingerprinting-explanation-detection-and-bypassing-it-in-playwright-and-puppeteer)
-This library precisely replicates the network behavior of real browsers.
+> [!NOTE]
+> This only covers the transport layer. It does not help bypass JavaScript-based challenges (Cloudflare Turnstile, Akamai sensor data, Kasada, etc.), CAPTCHA, or behavioral analysis β those require a different approach entirely
-## π¦ Installation
+## install
```bash
-# npm
npm install node-wreq
+```
-# yarn
-yarn add node-wreq
+## contents
-# pnpm
-pnpm add node-wreq
-```
+#### β‘ Β **[quick start](#quick-start)**
+#### π Β **[fetch](#fetch)**
+#### π§© Β **[client](#client)** β shared defaults, reusable config.
+#### π Β **[browser profiles](#browser-profiles)**
+#### πͺ Β **[hooks](#hooks)** β request lifecycle, dynamic auth, retries, etc.
+#### πͺ Β **[cookies and sessions](#cookies)**
+#### π Β **[redirects and retries](#redirects-and-retries)**
+#### π Β **[observability](#observability)**
+#### π¨ Β **[error handling](#errors)**
+#### π Β **[websockets](#websockets)**
+#### π§ͺ Β **[networking / transport knobs](#networking)** β TLS, HTTP/1, HTTP/2 options; header ordering.
-That's it! π Pre-built native modules are included for all major platforms:
-- π macOS (Intel & Apple Silicon)
-- π§ Linux (x64 & ARM64)
-- πͺ Windows (x64)
+## β‘ quick start
-## π» Usage
+```ts
+import { fetch } from 'node-wreq';
+
+const response = await fetch('https://httpbin.org/get', {
+ browser: 'chrome_137',
+});
+
+console.log(response.status);
+console.log(await response.json());
+```
-### Basic Request
+If you keep repeating config, move to a client:
-```typescript
-import { request } from 'node-wreq';
+```ts
+import { createClient } from 'node-wreq';
-const response = await request({
- url: 'https://example.com/api',
+const client = createClient({
+ baseURL: 'https://httpbin.org',
browser: 'chrome_137',
+ headers: {
+ 'x-client': 'node-wreq',
+ },
+ retry: 2,
+});
+
+const response = await client.fetch('/anything', {
+ query: { from: 'client' },
});
-console.log(response.status); // 200
-console.log(response.body); // Response body
-console.log(response.headers); // Response headers
-console.log(response.cookies); // Cookies
+console.log(response.status);
+console.log(await response.json());
```
-### With Custom Headers
+## π fetchΒ Β Β·Β Β [β](#contents)
+
+### simple GET
-```typescript
-import { request } from 'node-wreq';
+```ts
+import { fetch } from 'node-wreq';
-const response = await request({
- url: 'https://api.example.com/data',
+const response = await fetch('https://httpbin.org/get', {
browser: 'firefox_139',
+ query: {
+ source: 'node-wreq',
+ debug: true,
+ },
+ timeout: 15_000,
+});
+
+const body = await response.json();
+
+console.log(response.ok);
+console.log(body.args);
+```
+
+### JSON POST
+
+```ts
+import { fetch } from 'node-wreq';
+
+const response = await fetch('https://api.example.com/items', {
+ method: 'POST',
+ browser: 'chrome_137',
+ headers: {
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: 'example',
+ enabled: true,
+ }),
+ throwHttpErrors: true,
+});
+
+console.log(await response.json());
+```
+
+### build a `Request` first
+
+```ts
+import { Request, fetch } from 'node-wreq';
+
+const request = new Request('https://httpbin.org/post', {
+ method: 'POST',
headers: {
- 'Authorization': 'Bearer token123',
- 'Custom-Header': 'value',
+ 'content-type': 'application/json',
},
+ body: JSON.stringify({ via: 'Request' }),
+});
+
+const response = await fetch(request, {
+ browser: 'chrome_137',
+});
+
+console.log(await response.json());
+```
+
+### read extra metadata
+
+`fetch()` returns a fetch-style `Response`, plus extra metadata under `response.wreq`.
+
+```ts
+const response = await fetch('https://example.com', {
+ browser: 'chrome_137',
});
+
+console.log(response.status);
+console.log(response.headers.get('content-type'));
+
+console.log(response.wreq.cookies);
+console.log(response.wreq.setCookies);
+console.log(response.wreq.timings);
+console.log(response.wreq.redirectChain);
```
-### POST Request
+If you need a Node stream instead of a WHATWG stream:
+
+```ts
+const readable = response.wreq.readable();
+
+readable.pipe(process.stdout);
+```
+
+## π§© clientΒ Β Β·Β Β [β](#contents)
+
+Use `createClient(...)` when requests share defaults:
-```typescript
-import { post } from 'node-wreq';
+- `baseURL`
+- browser profile
+- headers
+- proxy
+- timeout
+- hooks
+- retry policy
+- cookie jar
-const response = await post(
- 'https://api.example.com/submit',
- JSON.stringify({ foo: 'bar' }),
+### shared defaults
+
+```ts
+import { createClient } from 'node-wreq';
+
+const client = createClient({
+ baseURL: 'https://api.example.com',
+ browser: 'chrome_137',
+ timeout: 10_000,
+ headers: {
+ authorization: `Bearer ${process.env.API_TOKEN}`,
+ },
+ retry: {
+ limit: 2,
+ statusCodes: [429, 503],
+ },
+});
+
+const users = await client.get('/users');
+
+console.log(await users.json());
+
+const created = await client.post(
+ '/users',
+ JSON.stringify({ email: 'user@example.com' }),
{
- browser: 'chrome_137',
headers: {
- 'Content-Type': 'application/json',
+ 'content-type': 'application/json',
},
}
);
+
+console.log(created.status);
```
-### Convenience Methods
+### extend a client
-```typescript
-import { get, post } from 'node-wreq';
+```ts
+const base = createClient({
+ baseURL: 'https://api.example.com',
+ browser: 'chrome_137',
+});
-// GET request
-const data = await get('https://api.example.com/users');
+const admin = base.extend({
+ headers: {
+ authorization: `Bearer ${process.env.ADMIN_TOKEN}`,
+ },
+});
-// POST request
-const result = await post(
- 'https://api.example.com/users',
- JSON.stringify({ name: 'John' })
-);
+await base.get('/health');
+await admin.get('/admin/stats');
```
-### With Proxy
+## π browser profilesΒ Β Β·Β Β [β](#contents)
-```typescript
-import { request } from 'node-wreq';
+Inspect the available profiles at runtime:
-const response = await request({
- url: 'https://example.com',
- browser: 'chrome_137',
- // proxy: 'http://proxy.example.com:8080',
- // proxy: 'http://username:password@proxy.example.com:8080',
- // proxy: 'socks5://proxy.example.com:1080',
+```ts
+import { getProfiles } from 'node-wreq';
+
+console.log(getProfiles());
+```
+
+There is also `BROWSER_PROFILES` if you want the generated list directly.
+
+Typical profiles include browser families like:
+
+- Chrome
+- Edge
+- Firefox
+- Safari
+- Opera
+- OkHttp
+
+## πͺ hooksΒ Β Β·Β Β [β](#contents)
+
+Hooks are the request pipeline.
+
+Available phases:
+
+- `init`
+- `beforeRequest`
+- `afterResponse`
+- `beforeRetry`
+- `beforeError`
+- `beforeRedirect`
+
+### common pattern: auth, tracing, proxy rotation
+
+```ts
+import { createClient } from 'node-wreq';
+
+const client = createClient({
+ baseURL: 'https://example.com',
+ retry: {
+ limit: 2,
+ statusCodes: [429, 503],
+ backoff: ({ attempt }) => attempt * 250,
+ },
+ hooks: {
+ init: [
+ ({ options, state }) => {
+ options.query = { ...options.query, source: 'hook-init' };
+
+ state.startedAt = Date.now();
+ },
+ ],
+ beforeRequest: [
+ ({ request, options, state }) => {
+ request.headers.set('x-trace-id', crypto.randomUUID());
+ request.headers.set('authorization', `Bearer ${getAccessToken()}`);
+
+ options.proxy = pickProxy();
+
+ state.lastProxy = options.proxy;
+ },
+ ],
+ beforeRetry: [
+ ({ options, attempt, error, state }) => {
+ options.proxy = pickProxy(attempt);
+
+ console.log('retrying', {
+ attempt,
+ proxy: options.proxy,
+ previousProxy: state.lastProxy,
+ error,
+ });
+ },
+ ],
+ beforeError: [
+ ({ error, state }) => {
+ error.message = `[trace=${String(state.startedAt)}] ${error.message}`;
+
+ return error;
+ },
+ ],
+ },
});
```
-### WebSocket Connection
+### replace a response in `afterResponse`
+
+```ts
+import { Response, fetch } from 'node-wreq';
+
+const response = await fetch('https://example.com/account', {
+ hooks: {
+ afterResponse: [
+ async ({ response }) => {
+ if (response.status === 401) {
+ return new Response(JSON.stringify({ guest: true }), {
+ status: 200,
+ headers: {
+ 'content-type': 'application/json',
+ },
+ url: response.url,
+ });
+ }
+ },
+ ],
+ },
+});
-```typescript
-import { websocket } from 'node-wreq';
+console.log(await response.json());
+```
+
+### mutate redirect hops
+
+```ts
+await fetch('https://example.com/login', {
+ hooks: {
+ beforeRedirect: [
+ ({ request, nextUrl, redirectCount }) => {
+ request.headers.set('x-redirect-hop', String(redirectCount));
+ request.headers.set('x-next-url', nextUrl);
+ },
+ ],
+ },
+});
+```
+
+Rule of thumb:
+
+- use hooks for dynamic behavior
+- use client defaults for static behavior
+
+## πͺ cookies and sessionsΒ Β Β·Β Β [β](#contents)
+
+`node-wreq` does not force a built-in cookie store.
+
+You provide a `cookieJar` with two methods:
+
+- `getCookies(url)`
+- `setCookie(cookie, url)`
+
+That jar can be:
+
+- in-memory
+- `tough-cookie`
+- Redis-backed
+- DB-backed
+- anything else that matches the interface
+
+### tiny in-memory jar
+
+```ts
+import { fetch, websocket } from 'node-wreq';
+
+const jarStore = new Map();
+
+const cookieJar = {
+ getCookies() {
+ return [...jarStore.entries()].map(([name, value]) => ({
+ name,
+ value,
+ }));
+ },
+ setCookie(cookie: string) {
+ const [pair] = cookie.split(';');
+ const [name, value = ''] = pair.split('=');
+
+ jarStore.set(name, value);
+ },
+};
+
+await fetch('https://example.com/login', { cookieJar });
+await fetch('https://example.com/profile', { cookieJar });
+await websocket('wss://example.com/ws', { cookieJar });
+```
+
+### `tough-cookie`
+
+```bash
+npm install tough-cookie
+```
+
+```ts
+import { CookieJar as ToughCookieJar } from 'tough-cookie';
+import { createClient } from 'node-wreq';
+
+const toughJar = new ToughCookieJar();
-const ws = await websocket({
- url: 'wss://echo.websocket.org',
+const cookieJar = {
+ async getCookies(url: string) {
+ const cookies = await toughJar.getCookies(url);
+
+ return cookies.map((cookie) => ({
+ name: cookie.key,
+ value: cookie.value,
+ }));
+ },
+ async setCookie(cookie: string, url: string) {
+ await toughJar.setCookie(cookie, url);
+ },
+};
+
+const client = createClient({
browser: 'chrome_137',
- onMessage: (data) => {
- console.log('Received:', data);
+ cookieJar,
+});
+
+await client.fetch('https://example.com/login');
+await client.fetch('https://example.com/profile');
+```
+
+### inspect cookies on a response
+
+```ts
+import { fetch } from 'node-wreq';
+
+const response = await fetch('https://example.com/login', { cookieJar });
+
+console.log(response.wreq.setCookies);
+console.log(response.wreq.cookies);
+```
+
+## π redirects and retriesΒ Β Β·Β Β [β](#contents)
+
+Both are opt-in controls on top of the normal request pipeline.
+
+### manual redirects
+
+```ts
+const response = await fetch('https://example.com/login', {
+ redirect: 'manual',
+});
+
+console.log(response.status);
+console.log(response.headers.get('location'));
+console.log(response.redirected);
+```
+
+Modes:
+
+- `follow` - default redirect following
+- `manual` - return the redirect response as-is
+- `error` - throw on the first redirect
+
+Useful redirect facts:
+
+- `response.wreq.redirectChain` records followed hops
+- `301` / `302` rewrite `POST` to `GET`
+- `303` rewrites to `GET` unless current method is `HEAD`
+- `307` / `308` preserve method and body
+- `authorization` is stripped on cross-origin redirect
+
+### simple retries
+
+```ts
+const response = await fetch('https://example.com', {
+ retry: 2,
+});
+```
+
+### explicit retry policy
+
+```ts
+const response = await fetch('https://example.com', {
+ retry: {
+ limit: 3,
+ statusCodes: [429, 503],
+ backoff: ({ attempt }) => attempt * 500,
},
- onClose: () => {
- console.log('Connection closed');
+});
+```
+
+### custom retry decision
+
+```ts
+import { TimeoutError, fetch } from 'node-wreq';
+
+const response = await fetch('https://example.com', {
+ retry: {
+ limit: 5,
+ shouldRetry: ({ error, response }) => {
+ if (response?.status === 429) {
+ return true;
+ }
+
+ return error instanceof TimeoutError;
+ },
},
- onError: (error) => {
- console.error('Error:', error);
+});
+```
+
+Defaults:
+
+- retry is off unless you enable it
+- default retry methods are `GET` and `HEAD`
+- default status codes include `408`, `425`, `429`, `500`, `502`, `503`, `504`
+- default error codes include `ECONNRESET`, `ECONNREFUSED`, `ETIMEDOUT`, `ERR_TIMEOUT`
+
+## π observabilityΒ Β Β·Β Β [β](#contents)
+
+Two main surfaces:
+
+- `response.wreq.timings`
+- `onStats(stats)`
+
+### per-request stats callback
+
+```ts
+await fetch('https://example.com', {
+ onStats: ({ attempt, timings, response, error }) => {
+ console.log({
+ attempt,
+ wait: timings.wait,
+ total: timings.total,
+ status: response?.status,
+ error,
+ });
},
});
+```
-// Send text message
-await ws.send('Hello!');
+### read timings from the final response
-// Send binary message
-await ws.send(Buffer.from([1, 2, 3]));
+```ts
+const response = await fetch('https://example.com', {
+ browser: 'chrome_137',
+});
-// Close connection
-await ws.close();
+console.log(response.wreq.timings);
```
-## π API Reference
+Current timings are wrapper-level timings that are still useful in practice:
+
+- request start
+- response available
+- total time when body consumption is known
-### `request(options:` [`RequestOptions`](#requestoptions)`): Promise<`[`Response`](#response)`>`
+## π¨ error handlingΒ Β Β·Β Β [β](#contents)
-Main function for making HTTP requests with browser impersonation.
+Main error classes:
-**Options:**
-
+- `RequestError`
+- `HTTPError`
+- `TimeoutError`
+- `AbortError`
+- `WebSocketError`
-```typescript
-interface RequestOptions {
- url: string; // Required: URL to request
- browser?: BrowserProfile; // Default: 'chrome_137'
- method?: HttpMethod; // Default: 'GET'
- headers?: Record;
- body?: string;
- proxy?: string; // HTTP/HTTPS/SOCKS5 proxy URL
- timeout?: number; // Default: 30000ms
+Typical patterns:
+
+```ts
+import { HTTPError, TimeoutError, fetch } from 'node-wreq';
+
+try {
+ await fetch('https://example.com', {
+ timeout: 1_000,
+ throwHttpErrors: true,
+ });
+} catch (error) {
+ if (error instanceof TimeoutError) {
+ console.error('request timed out');
+ } else if (error instanceof HTTPError) {
+ console.error('bad status', error.statusCode);
+ } else {
+ console.error(error);
+ }
}
```
-**Response:**
-
+## π websocketsΒ Β Β·Β Β [β](#contents)
-```typescript
-interface Response {
- status: number;
- headers: Record;
- body: string;
- cookies: Record;
- url: string; // Final URL after redirects
-}
+You can use either:
+
+- `await websocket(url, init?)`
+- `new WebSocket(url, init?)`
+
+### simple helper
+
+```ts
+import { websocket } from 'node-wreq';
+
+const socket = await websocket('wss://echo.websocket.events', {
+ browser: 'chrome_137',
+ protocols: ['chat'],
+});
+
+socket.addEventListener('message', (event) => {
+ console.log('message:', event.data);
+});
+
+socket.send('hello');
```
-### `get(url: string, options?): Promise<`[`Response`](#response)`>`
+### WHATWG-like constructor
-### `post(url: string, body?: string, options?): Promise<`[`Response`](#response)`>`
+```ts
+import { WebSocket } from 'node-wreq';
-### `websocket(options:` [`WebSocketOptions`](#websocketoptions)`): Promise`
+const socket = new WebSocket('wss://example.com/ws', {
+ binaryType: 'arraybuffer',
+});
+await socket.opened;
-**Options:**
-
+socket.onmessage = (event) => {
+ if (event.data instanceof ArrayBuffer) {
+ console.log(new Uint8Array(event.data));
+ }
+};
-```typescript
-interface WebSocketOptions {
- url: string; // Required: WebSocket URL (ws:// or wss://)
- browser?: BrowserProfile; // Default: 'chrome_137'
- headers?: Record;
- proxy?: string; // HTTP/HTTPS/SOCKS5 proxy URL
- onMessage: (data: string | Buffer) => void; // Required: Message callback
- onClose?: () => void; // Optional: Close callback
- onError?: (error: string) => void; // Optional: Error callback
-}
+socket.send(new Uint8Array([1, 2, 3]));
+socket.close(1000, 'done');
```
-**WebSocket Methods:**
+### websocket from a client
-```typescript
-class WebSocket {
- send(data: string | Buffer): Promise;
- close(): Promise;
-}
+Useful when you want shared defaults like browser, proxy, or cookies:
+
+```ts
+const client = createClient({
+ browser: 'chrome_137',
+ cookieJar: yourCookieJar,
+});
+
+const socket = await client.websocket('wss://example.com/ws');
```
-### `getProfiles():` [`BrowserProfile[]`](#browser-profiles)
+Notes:
-Get list of available browser profiles.
+- cookies from `cookieJar` are sent during handshake
+- duplicate subprotocols are rejected
-```typescript
-import { getProfiles } from 'node-wreq';
+## π§ͺ networking / transport knobsΒ Β Β·Β Β [β](#contents)
+
+This is the "transport nerd" section.
-const profiles = getProfiles();
+Everything else here is for debugging request shape, fingerprint-sensitive targets, or testing transport hypotheses.
-console.log(profiles);
-// ['chrome_100', 'chrome_101', ..., 'chrome_137', 'edge_101', ..., 'safari_18', ...]
+### browser profile + proxy + timeout
+
+```ts
+const response = await fetch('https://httpbin.org/anything', {
+ browser: 'chrome_137',
+ proxy: 'http://username:password@proxy.example.com:8080',
+ timeout: 10_000,
+});
```
-## π Browser Profiles
-
+### disable default browser-like headers
-Available browser profiles (78+ profiles):
+By default, `node-wreq` may apply profile-appropriate default headers.
-### Chrome
-29 versions from Chrome 100 to Chrome 137:
-- `chrome_100`, `chrome_101`, `chrome_104`, `chrome_105`, `chrome_106`, `chrome_107`, `chrome_108`, `chrome_109`, `chrome_110`
-- `chrome_114`, `chrome_116`, `chrome_117`, `chrome_118`, `chrome_119`, `chrome_120`, `chrome_123`, `chrome_124`, `chrome_126`
-- `chrome_127`, `chrome_128`, `chrome_129`, `chrome_130`, `chrome_131`, `chrome_132`, `chrome_133`, `chrome_134`, `chrome_135`, `chrome_136`, `chrome_137`
+If you want full manual control:
-### Edge
-5 versions: `edge_101`, `edge_122`, `edge_127`, `edge_131`, `edge_134`
+```ts
+await fetch('https://example.com', {
+ disableDefaultHeaders: true,
+ headers: {
+ accept: '*/*',
+ 'user-agent': 'custom-client',
+ },
+});
+```
-### Safari
-19 versions including iOS and iPad:
-- Desktop: `safari_15_3`, `safari_15_5`, `safari_15_6_1`, `safari_16`, `safari_16_5`, `safari_17_0`, `safari_17_2_1`, `safari_17_4_1`, `safari_17_5`, `safari_18`, `safari_18_2`, `safari_18_3`, `safari_18_3_1`, `safari_18_5`
-- iOS: `safari_ios_16_5`, `safari_ios_17_2`, `safari_ios_17_4_1`, `safari_ios_18_1_1`
-- iPad: `safari_ipad_18`
+### exact header order
-### Firefox
-10 versions including private and Android:
-- `firefox_109`, `firefox_117`, `firefox_128`, `firefox_133`, `firefox_135`, `firefox_136`, `firefox_139`
-- Private: `firefox_private_135`, `firefox_private_136`
-- Android: `firefox_android_135`
+Use tuples when header order matters:
-### Opera
-4 versions: `opera_116`, `opera_117`, `opera_118`, `opera_119`
+```ts
+await fetch('https://example.com', {
+ headers: [
+ ['x-lower', 'one'],
+ ['X-Mixed', 'two'],
+ ],
+});
+```
-### OkHttp (Android HTTP client)
-8 versions: `okhttp_3_9`, `okhttp_3_11`, `okhttp_3_13`, `okhttp_3_14`, `okhttp_4_9`, `okhttp_4_10`, `okhttp_4_12`, `okhttp_5`
+### exact original header names on the wire
-> Use `getProfiles()` to get the complete list programmatically.
+Use this only if you really need exact casing / spelling preservation:
-## π Documentation
+```ts
+await fetch('https://example.com', {
+ disableDefaultHeaders: true,
+ keepOriginalHeaderNames: true,
+ headers: [
+ ['x-lower', 'one'],
+ ['X-Mixed', 'two'],
+ ],
+});
+```
-- **[Architecture Guide](docs/ARCHITECTURE.md)** β Technical details about TLS/HTTP2 fingerprinting, how browser impersonation works
-- **[Build Instructions](docs/BUILD.md)** β Developer guide for building from source
-- **[Publishing Guide](docs/PUBLISHING.md)** β How to publish the package
+### lower-level transport tuning
-## π€ Contributions are welcome!
+If a browser preset gets you close but not all the way there:
-Please read [Contributing Guide](CONTRIBUTING.md).
+```ts
+await fetch('https://example.com', {
+ browser: 'chrome_137',
+ tlsOptions: {
+ greaseEnabled: true,
+ },
+ http1Options: {
+ writev: true,
+ },
+ http2Options: {
+ adaptiveWindow: false,
+ maxConcurrentStreams: 64,
+ },
+});
+```
-## π Acknowledgments
+Use these only when:
-Built with:
-- [wreq](https://github.com/0x676e67/wreq) β Rust HTTP client with browser impersonation
-- [Neon](https://neon-bindings.com/) β Rust β Node.js bindings
+- a target is still picky after choosing a browser profile
+- you are comparing transport behavior
+- you want to debug fingerprint mismatches
+
+### compression
+
+Compression is enabled by default.
+
+Disable it if you need stricter control over response handling:
+
+```ts
+await fetch('https://example.com/archive', {
+ compress: false,
+});
+```
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 2584879..63dec09 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -5,7 +5,7 @@
```
βββββββββββββββββββββββββββββββββββββββ
β JavaScript/TypeScript Code β
-β request({ url, browser }) β
+β fetch(url, init) / createClient() β
ββββββββββββββββ¬βββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββ
@@ -58,18 +58,18 @@ The client automatically reuses connections (HTTP/2 keep-alive).
### Parallel Requests
-```typescript
-// Good: Parallel requests
+```ts
+// Good: parallel requests
const [res1, res2, res3] = await Promise.all([
- request({ url: url1 }),
- request({ url: url2 }),
- request({ url: url3 }),
+ fetch(url1),
+ fetch(url2),
+ fetch(url3),
]);
-// Bad: Sequential requests
-const res1 = await request({ url: url1 });
-const res2 = await request({ url: url2 });
-const res3 = await request({ url: url3 });
+// Slower: sequential requests
+const res1 = await fetch(url1);
+const res2 = await fetch(url2);
+const res3 = await fetch(url3);
```
## References
diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md
index b1eeadb..fb733c6 100644
--- a/docs/PUBLISHING.md
+++ b/docs/PUBLISHING.md
@@ -2,27 +2,42 @@
## How to Publish node-wreq
-This package uses a **single npm package** that includes pre-built native binaries for all supported platforms.
+Publishing is split into two layers:
+
+- scoped platform packages with the native `.node` binaries
+- the main `node-wreq` package with JS, types, loader logic, and `optionalDependencies`
+
+The publish version can be overridden at staging time with `NODE_WREQ_PUBLISH_VERSION`.
+That lets CI publish from the GitHub release tag even if the repository's root `package.json`
+stays on a placeholder or development version locally.
### Package Structure
When published, the package includes:
-- TypeScript compiled code (`dist/`)
-- Native binaries for all platforms (`rust/*.node`)
-
-Supported platforms:
-- π macOS Intel (x64)
-- π macOS Apple Silicon (arm64)
-- π§ Linux x64
-- πͺ Windows x64
+- main package:
+ - CommonJS output in `dist/`
+ - ESM wrapper output in `dist/`
+ - Type declarations in `dist/`
+ - `optionalDependencies` pointing at the scoped native packages
+- scoped platform packages:
+ - one native `.node` binary each
+
+Scoped package names:
+
+- `@node-wreq/darwin-x64`
+- `@node-wreq/darwin-arm64`
+- `@node-wreq/linux-x64-gnu`
+- `@node-wreq/linux-arm64-gnu`
+- `@node-wreq/linux-x64-musl`
+- `@node-wreq/win32-x64-msvc`
### Publishing Process
#### 1. Prerequisites
-- npm account with publish permissions
+- npm account with package publish permissions
- GitHub repository set up
-- `NPM_TOKEN` configured in GitHub Secrets
+- npm trusted publishing configured for each package you publish from this workflow
#### 1. Update Version
@@ -49,10 +64,24 @@ Then create a GitHub Release from this tag. This will trigger the build workflow
#### 3. Automated Build & Publish
GitHub Actions will automatically:
-1. Build native binaries for all platforms (macOS, Linux, Windows)
-2. Collect all binaries into the `rust/` directory
-3. Build TypeScript code
-4. Publish the package to npm with all binaries included
+1. Build native binaries for all configured platforms
+2. Publish one scoped package per platform artifact
+3. Build the JS outputs
+4. Stage the main package with generated `optionalDependencies`
+5. Publish the main package to npm
+
+Workflow requirements for trusted publishing:
+
+- the trusted publisher must point at this repository and the exact workflow filename: `.github/workflows/build.yml`
+- publish jobs must run on GitHub-hosted runners
+- publish jobs need `permissions.id-token: write`
+- no `NPM_TOKEN` is required for the `npm publish` steps
+
+Release tag behavior:
+
+- prereleases publish with npm dist-tag `rc`
+- stable releases publish with npm dist-tag `latest`
+- the published package version is derived from the GitHub release tag, for example `v1.0.0-rc2` -> `1.0.0-rc2`
### Local Testing Before Publishing
@@ -63,33 +92,42 @@ npm run build
# Run tests
npm test
-# Pack to see what will be published
-npm pack
-
-# Extract and inspect
-tar -tzf node-wreq-*.tgz
+# Stage the publishable main package
+npm run prepare:publish:main -- .release/main-package
-# Test in another project
-cd /path/to/test-project
-npm install /path/to/node-wreq/node-wreq-*.tgz
+# Inspect staged files
+find .release/main-package -maxdepth 3 -type f | sort
```
-### Manual Publishing (Not Recommended)
+### Manual Publishing (Platform Packages)
-If you need to publish manually:
+If you really need to publish a scoped platform package manually:
```bash
-# Build TypeScript
-npm run build:ts
+# Example: build a target
+npm run build:rust -- --target x86_64-unknown-linux-musl
+
+# Stage the scoped package
+NODE_WREQ_PUBLISH_VERSION=1.0.0-rc2 node ./scripts/prepare-platform-package.mjs \
+ --target x86_64-unknown-linux-musl \
+ --binary rust/node-wreq.linux-x64-musl.node \
+ --outDir .release/linux-x64-musl
+
+# Publish it
+npm publish .release/linux-x64-musl --access public
+```
-# Ensure all platform binaries are in rust/ directory
-ls rust/*.node
+### Manual Publishing (Main Package)
-# Publish
-npm publish --access public
+After the platform packages for the same version exist:
+
+```bash
+npm run build:ts
+NODE_WREQ_PUBLISH_VERSION=1.0.0-rc2 npm run prepare:publish:main -- .release/main-package
+npm publish .release/main-package --access public
```
-**Note:** Manual publishing requires you to have all platform binaries built locally, which is difficult without cross-compilation setup. Use GitHub Actions instead.
+Use GitHub Actions unless you have a specific reason not to.
## Troubleshooting
@@ -101,6 +139,6 @@ npm publish --access public
### Module Load Error After Install
-- Verify `rust/*.node` files are included in the published package
-- Check that the binary was built for the user's platform
-- Ensure file permissions are correct
+- Verify the matching scoped package for the user's platform was published
+- Verify the main package version and platform package versions match
+- Check that the loader can resolve the correct package for the user's platform
diff --git a/package-lock.json b/package-lock.json
index 67bc8ce..c638e9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "node-wreq",
- "version": "0.2.0",
+ "version": "0.0.0-reserved.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "node-wreq",
- "version": "0.2.0",
+ "version": "0.0.0-reserved.0",
"cpu": [
"x64",
"arm64"
@@ -19,203 +19,1376 @@
],
"devDependencies": {
"@napi-rs/cli": "^2.18.0",
+ "@stylistic/eslint-plugin": "^5.10.0",
"@types/node": "^20.0.0",
- "prettier": "^3.2.5",
- "rimraf": "^6.0.1",
- "typescript": "^5.0.0"
+ "@types/ws": "^8.18.1",
+ "oxfmt": "^0.41.0",
+ "oxlint": "^1.56.0",
+ "typescript": "^5.0.0",
+ "ws": "^8.18.3"
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">=18.0.0"
}
},
- "node_modules/@isaacs/balanced-match": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
- "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz",
+ "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
+ "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@napi-rs/cli": {
+ "version": "2.18.4",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "napi": "scripts/index.js"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxfmt/binding-android-arm-eabi": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.41.0.tgz",
+ "integrity": "sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-android-arm64": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.41.0.tgz",
+ "integrity": "sha512-s0b1dxNgb2KomspFV2LfogC2XtSJB42POXF4bMCLJyvQmAGos4ZtjGPfQreToQEaY0FQFjz3030ggI36rF1q5g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-darwin-arm64": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.41.0.tgz",
+ "integrity": "sha512-EGXGualADbv/ZmamE7/2DbsrYmjoPlAmHEpTL4vapLF4EfVD6fr8/uQDFnPJkUBjiSWFJZtFNsGeN1B6V3owmA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-darwin-x64": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.41.0.tgz",
+ "integrity": "sha512-WxySJEvdQQYMmyvISH3qDpTvoS0ebnIP63IMxLLWowJyPp/AAH0hdWtlo+iGNK5y3eVfa5jZguwNaQkDKWpGSw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-freebsd-x64": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.41.0.tgz",
+ "integrity": "sha512-Y2kzMkv3U3oyuYaR4wTfGjOTYTXiFC/hXmG0yVASKkbh02BJkvD98Ij8bIevr45hNZ0DmZEgqiXF+9buD4yMYQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.41.0.tgz",
+ "integrity": "sha512-ptazDjdUyhket01IjPTT6ULS1KFuBfTUU97osTP96X5y/0oso+AgAaJzuH81oP0+XXyrWIHbRzozSAuQm4p48g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-arm-musleabihf": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.41.0.tgz",
+ "integrity": "sha512-UkoL2OKxFD+56bPEBcdGn+4juTW4HRv/T6w1dIDLnvKKWr6DbarB/mtHXlADKlFiJubJz8pRkttOR7qjYR6lTA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-arm64-gnu": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.41.0.tgz",
+ "integrity": "sha512-gofu0PuumSOHYczD8p62CPY4UF6ee+rSLZJdUXkpwxg6pILiwSDBIouPskjF/5nF3A7QZTz2O9KFNkNxxFN9tA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-arm64-musl": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.41.0.tgz",
+ "integrity": "sha512-VfVZxL0+6RU86T8F8vKiDBa+iHsr8PAjQmKGBzSCAX70b6x+UOMFl+2dNihmKmUwqkCazCPfYjt6SuAPOeQJ3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-ppc64-gnu": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.41.0.tgz",
+ "integrity": "sha512-bwzokz2eGvdfJbc0i+zXMJ4BBjQPqg13jyWpEEZDOrBCQ91r8KeY2Mi2kUeuMTZNFXju+jcAbAbpyJxRGla0eg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-riscv64-gnu": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.41.0.tgz",
+ "integrity": "sha512-POLM//PCH9uqDeNDwWL3b3DkMmI3oI2cU6hwc2lnztD1o7dzrQs3R9nq555BZ6wI7t2lyhT9CS+CRaz5X0XqLA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-riscv64-musl": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.41.0.tgz",
+ "integrity": "sha512-NNK7PzhFqLUwx/G12Xtm6scGv7UITvyGdAR5Y+TlqsG+essnuRWR4jRNODWRjzLZod0T3SayRbnkSIWMBov33w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-s390x-gnu": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.41.0.tgz",
+ "integrity": "sha512-qVf/zDC5cN9eKe4qI/O/m445er1IRl6swsSl7jHkqmOSVfknwCe5JXitYjZca+V/cNJSU/xPlC5EFMabMMFDpw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-x64-gnu": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.41.0.tgz",
+ "integrity": "sha512-ojxYWu7vUb6ysYqVCPHuAPVZHAI40gfZ0PDtZAMwVmh2f0V8ExpPIKoAKr7/8sNbAXJBBpZhs2coypIo2jJX4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-x64-musl": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.41.0.tgz",
+ "integrity": "sha512-O2exZLBxoCMIv2vlvcbkdedazJPTdG0VSup+0QUCfYQtx751zCZNboX2ZUOiQ/gDTdhtXvSiot0h6GEGkOyalA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-openharmony-arm64": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.41.0.tgz",
+ "integrity": "sha512-N+31/VoL+z+NNBt8viy3I4NaIdPbiYeOnB884LKqvXldaE2dRztdPv3q5ipfZYv0RwFp7JfqS4I27K/DSHCakg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-win32-arm64-msvc": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.41.0.tgz",
+ "integrity": "sha512-Z7NAtu/RN8kjCQ1y5oDD0nTAeRswh3GJ93qwcW51srmidP7XPBmZbLlwERu1W5veCevQJtPS9xmkpcDTYsGIwQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-win32-ia32-msvc": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.41.0.tgz",
+ "integrity": "sha512-uNxxP3l4bJ6VyzIeRqCmBU2Q0SkCFgIhvx9/9dJ9V8t/v+jP1IBsuaLwCXGR8JPHtkj4tFp+RHtUmU2ZYAUpMA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-win32-x64-msvc": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.41.0.tgz",
+ "integrity": "sha512-49ZSpbZ1noozyPapE8SUOSm3IN0Ze4b5nkO+4+7fq6oEYQQJFhE0saj5k/Gg4oewVPdjn0L3ZFeWk2Vehjcw7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm-eabi": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.56.0.tgz",
+ "integrity": "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-android-arm64": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.56.0.tgz",
+ "integrity": "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-arm64": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.56.0.tgz",
+ "integrity": "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-darwin-x64": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.56.0.tgz",
+ "integrity": "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-freebsd-x64": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.56.0.tgz",
+ "integrity": "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.56.0.tgz",
+ "integrity": "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.56.0.tgz",
+ "integrity": "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.56.0.tgz",
+ "integrity": "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-arm64-musl": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.56.0.tgz",
+ "integrity": "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.56.0.tgz",
+ "integrity": "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.56.0.tgz",
+ "integrity": "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.56.0.tgz",
+ "integrity": "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.56.0.tgz",
+ "integrity": "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.56.0.tgz",
+ "integrity": "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-linux-x64-musl": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.56.0.tgz",
+ "integrity": "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-openharmony-arm64": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.56.0.tgz",
+ "integrity": "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.56.0.tgz",
+ "integrity": "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.56.0.tgz",
+ "integrity": "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.56.0.tgz",
+ "integrity": "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.10.0.tgz",
+ "integrity": "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/types": "^8.56.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "estraverse": "^5.3.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "eslint": "^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.21",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
+ "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz",
+ "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.4",
+ "@eslint/config-helpers": "^0.5.4",
+ "@eslint/core": "^1.2.0",
+ "@eslint/plugin-kit": "^0.7.0",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
"engines": {
- "node": "20 || >=22"
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/@isaacs/brace-expansion": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
- "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-3-Clause",
+ "peer": true,
"dependencies": {
- "@isaacs/balanced-match": "^4.0.1"
+ "estraverse": "^5.1.0"
},
"engines": {
- "node": "20 || >=22"
+ "node": ">=0.10"
}
},
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
- "license": "ISC",
+ "license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ "estraverse": "^5.2.0"
},
"engines": {
- "node": ">=12"
+ "node": ">=4.0"
}
},
- "node_modules/@napi-rs/cli": {
- "version": "2.18.4",
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
- "license": "MIT",
- "bin": {
- "napi": "scripts/index.js"
- },
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">= 10"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
+ "node": ">=4.0"
}
},
- "node_modules/@types/node": {
- "version": "20.19.21",
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.21.0"
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
}
},
- "node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=12"
+ "peer": true
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "flat-cache": "^4.0.0"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "engines": {
+ "node": ">=16.0.0"
}
},
- "node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
"engines": {
- "node": ">=12"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "color-name": "~1.1.4"
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
},
"engines": {
- "node": ">=7.0.0"
+ "node": ">=16"
}
},
- "node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
- "license": "MIT"
+ "license": "ISC",
+ "peer": true
},
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
- "license": "MIT",
+ "license": "ISC",
+ "peer": true,
"dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
+ "is-glob": "^4.0.3"
},
"engines": {
- "node": ">= 8"
+ "node": ">=10.13.0"
}
},
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">= 4"
+ }
},
- "node_modules/foreground-child": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.6",
- "signal-exit": "^4.0.1"
- },
+ "license": "MIT",
+ "peer": true,
"engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": ">=0.8.19"
}
},
- "node_modules/glob": {
- "version": "11.0.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
- "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.3.1",
- "jackspeak": "^4.1.1",
- "minimatch": "^10.0.3",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^2.0.0"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
+ "license": "MIT",
+ "peer": true,
"engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": ">=0.10.0"
}
},
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
"engines": {
- "node": ">=8"
+ "node": ">=0.10.0"
}
},
"node_modules/isexe": {
@@ -223,266 +1396,351 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
- "license": "ISC"
+ "license": "ISC",
+ "peer": true
},
- "node_modules/jackspeak": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
- "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
- "license": "BlueOak-1.0.0",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
"dependencies": {
- "@isaacs/cliui": "^8.0.2"
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
},
"engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": ">= 0.8.0"
}
},
- "node_modules/lru-cache": {
- "version": "11.2.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
- "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
"engines": {
- "node": "20 || >=22"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": {
- "version": "10.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
- "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
+ "peer": true,
"dependencies": {
- "@isaacs/brace-expansion": "^5.0.0"
+ "brace-expansion": "^5.0.5"
},
"engines": {
- "node": "20 || >=22"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
+ "license": "MIT",
+ "peer": true
},
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
- "license": "BlueOak-1.0.0"
+ "license": "MIT",
+ "peer": true
},
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
"engines": {
- "node": ">=8"
+ "node": ">= 0.8.0"
}
},
- "node_modules/path-scurry": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
- "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
+ "node_modules/oxfmt": {
+ "version": "0.41.0",
+ "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.41.0.tgz",
+ "integrity": "sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg==",
"dev": true,
- "license": "BlueOak-1.0.0",
+ "license": "MIT",
"dependencies": {
- "lru-cache": "^11.0.0",
- "minipass": "^7.1.2"
+ "tinypool": "2.1.0"
+ },
+ "bin": {
+ "oxfmt": "bin/oxfmt"
},
"engines": {
- "node": "20 || >=22"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxfmt/binding-android-arm-eabi": "0.41.0",
+ "@oxfmt/binding-android-arm64": "0.41.0",
+ "@oxfmt/binding-darwin-arm64": "0.41.0",
+ "@oxfmt/binding-darwin-x64": "0.41.0",
+ "@oxfmt/binding-freebsd-x64": "0.41.0",
+ "@oxfmt/binding-linux-arm-gnueabihf": "0.41.0",
+ "@oxfmt/binding-linux-arm-musleabihf": "0.41.0",
+ "@oxfmt/binding-linux-arm64-gnu": "0.41.0",
+ "@oxfmt/binding-linux-arm64-musl": "0.41.0",
+ "@oxfmt/binding-linux-ppc64-gnu": "0.41.0",
+ "@oxfmt/binding-linux-riscv64-gnu": "0.41.0",
+ "@oxfmt/binding-linux-riscv64-musl": "0.41.0",
+ "@oxfmt/binding-linux-s390x-gnu": "0.41.0",
+ "@oxfmt/binding-linux-x64-gnu": "0.41.0",
+ "@oxfmt/binding-linux-x64-musl": "0.41.0",
+ "@oxfmt/binding-openharmony-arm64": "0.41.0",
+ "@oxfmt/binding-win32-arm64-msvc": "0.41.0",
+ "@oxfmt/binding-win32-ia32-msvc": "0.41.0",
+ "@oxfmt/binding-win32-x64-msvc": "0.41.0"
}
},
- "node_modules/prettier": {
- "version": "3.6.2",
+ "node_modules/oxlint": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.56.0.tgz",
+ "integrity": "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g==",
"dev": true,
"license": "MIT",
"bin": {
- "prettier": "bin/prettier.cjs"
+ "oxlint": "bin/oxlint"
},
"engines": {
- "node": ">=14"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
- "url": "https://github.com/prettier/prettier?sponsor=1"
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxlint/binding-android-arm-eabi": "1.56.0",
+ "@oxlint/binding-android-arm64": "1.56.0",
+ "@oxlint/binding-darwin-arm64": "1.56.0",
+ "@oxlint/binding-darwin-x64": "1.56.0",
+ "@oxlint/binding-freebsd-x64": "1.56.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.56.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.56.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.56.0",
+ "@oxlint/binding-linux-arm64-musl": "1.56.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.56.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.56.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.56.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.56.0",
+ "@oxlint/binding-linux-x64-gnu": "1.56.0",
+ "@oxlint/binding-linux-x64-musl": "1.56.0",
+ "@oxlint/binding-openharmony-arm64": "1.56.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.56.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.56.0",
+ "@oxlint/binding-win32-x64-msvc": "1.56.0"
+ },
+ "peerDependencies": {
+ "oxlint-tsgolint": ">=0.15.0"
+ },
+ "peerDependenciesMeta": {
+ "oxlint-tsgolint": {
+ "optional": true
+ }
}
},
- "node_modules/rimraf": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
- "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
+ "peer": true,
"dependencies": {
- "glob": "^11.0.0",
- "package-json-from-dist": "^1.0.0"
- },
- "bin": {
- "rimraf": "dist/esm/bin.mjs"
+ "yocto-queue": "^0.1.0"
},
"engines": {
- "node": "20 || >=22"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "shebang-regex": "^3.0.0"
+ "p-limit": "^3.0.2"
},
"engines": {
- "node": ">=8"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=8"
}
},
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
- "license": "ISC",
+ "license": "MIT",
+ "peer": true,
"engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
+ "node": ">=8"
}
},
- "node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
"engines": {
"node": ">=12"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
+ "peer": true,
"engines": {
- "node": ">=8"
+ "node": ">= 0.8.0"
}
},
- "node_modules/string-width-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
- "node": ">=8"
+ "node": ">=6"
}
},
- "node_modules/string-width-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/string-width-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "ansi-regex": "^5.0.1"
+ "shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
- "node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
+ "peer": true,
"engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ "node": ">=8"
}
},
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "node_modules/tinypool": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz",
+ "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
"engines": {
- "node": ">=8"
+ "node": "^20.0.0 || >=22.0.0"
}
},
- "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
"engines": {
- "node": ">=8"
+ "node": ">= 0.8.0"
}
},
"node_modules/typescript": {
@@ -502,12 +1760,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
+ "peer": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -518,102 +1788,51 @@
"node": ">= 8"
}
},
- "node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
+ "peer": true,
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ "node": ">=0.10.0"
}
},
- "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
+ "node": ">=10.0.0"
},
- "engines": {
- "node": ">=8"
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
},
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
}
},
- "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/wrap-ansi-cjs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
+ "peer": true,
"engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
+ "node": ">=10"
},
- "engines": {
- "node": ">=8"
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
}
}
diff --git a/package.json b/package.json
index 38238a2..7a644bf 100644
--- a/package.json
+++ b/package.json
@@ -1,56 +1,81 @@
{
"name": "node-wreq",
- "version": "0.2.0",
- "description": "Browser fingerprint bypass library using Rust for TLS/HTTP2 impersonation",
- "main": "dist/node-wreq.js",
+ "version": "0.0.0-reserved.0",
+ "description": "HTTP client with native TLS, HTTP2, JA3, JA4 browser impersonation backed by wreq's Rust core",
+ "main": "./dist/node-wreq.js",
+ "module": "./dist/node-wreq.mjs",
"types": "dist/node-wreq.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/node-wreq.d.ts",
+ "require": "./dist/node-wreq.js",
+ "import": "./dist/node-wreq.mjs",
+ "default": "./dist/node-wreq.js"
+ },
+ "./package.json": "./package.json"
+ },
"scripts": {
"build": "npm run build:rust && npm run build:ts",
"build:rust": "napi build --platform --release --cargo-cwd rust rust",
- "build:ts": "npm run clean:dist && tsc",
- "clean:dist": "rimraf dist",
+ "build:ts": "node ./scripts/generate-browser-profiles.mjs && tsc && node ./scripts/postbuild.mjs",
+ "prepare": "node ./scripts/install-git-hooks.mjs",
+ "prepare:publish:main": "node ./scripts/prepare-main-package.mjs",
+ "prepare:publish:platform": "node ./scripts/prepare-platform-package.mjs",
"artifacts": "napi artifacts",
- "clean": "rimraf dist rust/target rust/*.node",
- "test": "npm run build && node --test dist/test/*.spec.js",
- "format": "prettier --write \"src/**/*.{ts,js,json,md}\"",
- "format:check": "prettier --check \"src/**/*.{ts,js,json,md}\""
+ "clean": "rm -rf dist rust/target rust/*.node",
+ "test": "npm run build && node --test dist/test/node-wreq.spec.js",
+ "lint": "oxlint --deny-warnings src",
+ "lint:fix": "oxlint --fix src",
+ "format": "oxfmt --write \"src/**/*.ts\"",
+ "format:check": "oxfmt --check \"src/**/*.ts\""
},
"keywords": [
+ "anti-bot",
+ "bypass",
"browser",
+ "browser-fingerprint-bypass",
+ "browseer-impersonation",
+ "browser-emulation",
+ "crawler",
+ "cloudflare",
+ "cloudflare-bypass",
+ "curl-impersonate",
+ "scrapper",
"fingerprint",
- "bypass",
- "anti-bot",
"tls",
+ "tls-fingerprint",
"http2",
+ "fetch",
+ "websocket",
"impersonation",
"web-scraping",
- "crawler",
"web-scraper",
"wreq",
"ja3",
- "tls-fingerprint",
- "ja4",
- "browser-fingerprint-bypass"
+ "ja4"
],
- "author": "will-work-for-meal",
+ "author": "StopMakingThatBigFace",
"license": "MIT",
"repository": {
"type": "git",
- "url": "git+https://github.com/will-work-for-meal/node-wreq.git"
+ "url": "git+https://github.com/StopMakingThatBigFace/node-wreq.git"
},
"bugs": {
- "url": "https://github.com/will-work-for-meal/node-wreq/issues"
+ "url": "https://github.com/StopMakingThatBigFace/node-wreq/issues"
},
- "homepage": "https://github.com/will-work-for-meal/node-wreq#readme",
+ "homepage": "https://github.com/StopMakingThatBigFace/node-wreq#readme",
"devDependencies": {
"@napi-rs/cli": "^2.18.0",
+ "@stylistic/eslint-plugin": "^5.10.0",
"@types/node": "^20.0.0",
- "prettier": "^3.2.5",
- "rimraf": "^6.0.1",
- "typescript": "^5.0.0"
+ "@types/ws": "^8.18.1",
+ "oxfmt": "^0.41.0",
+ "oxlint": "^1.56.0",
+ "typescript": "^5.0.0",
+ "ws": "^8.18.3"
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">=18.0.0"
},
"os": [
"darwin",
@@ -73,6 +98,8 @@
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-unknown-linux-gnu",
+ "aarch64-unknown-linux-gnu",
+ "x86_64-unknown-linux-musl",
"x86_64-pc-windows-msvc"
]
}
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 0482723..5805c06 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 4
-[[package]]
-name = "addr2line"
-version = "0.25.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
-dependencies = [
- "gimli",
-]
-
[[package]]
name = "adler2"
version = "2.0.1"
@@ -78,27 +69,6 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
-[[package]]
-name = "autocfg"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
-
-[[package]]
-name = "backtrace"
-version = "0.3.76"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
-dependencies = [
- "addr2line",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
- "windows-link",
-]
-
[[package]]
name = "bindgen"
version = "0.72.1"
@@ -134,11 +104,10 @@ dependencies = [
[[package]]
name = "boring-sys2"
-version = "5.0.0-alpha.10"
+version = "5.0.0-alpha.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fc6cf07c95de5849d3e42032e2bc35242f2b9cffdd1835ee4e5d2b7a0ec65c7"
+checksum = "455d79965f5155dcc88a7abce112c3590883889131b799beda10bf9a813ed669"
dependencies = [
- "autocfg",
"bindgen",
"cmake",
"fs_extra",
@@ -147,9 +116,9 @@ dependencies = [
[[package]]
name = "boring2"
-version = "5.0.0-alpha.10"
+version = "5.0.0-alpha.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38a24a6ad62dc302105299550ef2f09ebaf82be4d60bff4ceabc36146a35322b"
+checksum = "183ccc3854411c035410dcdbffafca62084f3a6c33f013c77e83c025d2a08a28"
dependencies = [
"bitflags",
"boring-sys2",
@@ -187,9 +156,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
-version = "1.2.41"
+version = "1.2.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
+checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -225,9 +194,9 @@ dependencies = [
[[package]]
name = "cmake"
-version = "0.1.54"
+version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
@@ -242,8 +211,6 @@ dependencies = [
"compression-core",
"flate2",
"memchr",
- "zstd",
- "zstd-safe",
]
[[package]]
@@ -315,6 +282,17 @@ dependencies = [
"crypto-common",
]
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "either"
version = "1.15.0"
@@ -329,9 +307,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
-version = "0.1.4"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flate2"
@@ -416,17 +394,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
-[[package]]
-name = "futures-macro"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
[[package]]
name = "futures-sink"
version = "0.3.31"
@@ -446,7 +413,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
- "futures-macro",
"futures-sink",
"futures-task",
"pin-project-lite",
@@ -476,12 +442,6 @@ dependencies = [
"wasi 0.14.7+wasi-0.2.4",
]
-[[package]]
-name = "gimli"
-version = "0.32.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
-
[[package]]
name = "glob"
version = "0.3.3"
@@ -500,6 +460,12 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
[[package]]
name = "http"
version = "1.3.1"
@@ -536,9 +502,9 @@ dependencies = [
[[package]]
name = "http2"
-version = "0.5.8"
+version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c7ed027cf2bcf07fcd4e27cfba91b8e5fbae47151283406b16391c463e3a6db"
+checksum = "a7349aa548b6a413a7e7146d5d7e3db4565b3d0fbc8ff53c72e46b44b083cf9f"
dependencies = [
"atomic-waker",
"bytes",
@@ -547,6 +513,7 @@ dependencies = [
"futures-sink",
"http",
"indexmap",
+ "parking_lot",
"slab",
"smallvec",
"tokio",
@@ -560,42 +527,124 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
-name = "indexmap"
-version = "2.11.4"
+name = "icu_collections"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
- "equivalent",
- "hashbrown 0.16.0",
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
]
[[package]]
-name = "io-uring"
-version = "0.7.10"
+name = "icu_locale_core"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
- "bitflags",
- "cfg-if",
- "libc",
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
]
[[package]]
-name = "ipnet"
-version = "2.11.0"
+name = "icu_normalizer"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
[[package]]
-name = "iri-string"
-version = "0.7.8"
+name = "icu_normalizer_data"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [
- "memchr",
- "serde",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.0",
]
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
[[package]]
name = "itertools"
version = "0.13.0"
@@ -623,9 +672,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.177"
+version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "libloading"
@@ -657,6 +706,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -696,9 +751,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.0.4"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
@@ -737,11 +792,10 @@ name = "node-wreq"
version = "0.1.0"
dependencies = [
"anyhow",
- "futures-util",
"neon",
- "once_cell",
"serde",
"serde_json",
+ "strum",
"thiserror 1.0.69",
"tokio",
"wreq",
@@ -764,15 +818,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
-[[package]]
-name = "object"
-version = "0.37.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
-dependencies = [
- "memchr",
-]
-
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -837,6 +882,15 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -863,9 +917,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.41"
+version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
@@ -943,12 +997,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298"
-[[package]]
-name = "rustc-demangle"
-version = "0.1.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
-
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -1039,18 +1087,6 @@ dependencies = [
"serde_core",
]
-[[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
-dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
-]
-
[[package]]
name = "sha1"
version = "0.10.6"
@@ -1097,14 +1133,38 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
-version = "0.6.0"
+version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys",
]
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "strum"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
+
+[[package]]
+name = "strum_macros"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "syn"
version = "2.0.106"
@@ -1122,6 +1182,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -1193,21 +1264,28 @@ dependencies = [
"time-core",
]
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
[[package]]
name = "tokio"
-version = "1.47.1"
+version = "1.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
+checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
dependencies = [
- "backtrace",
"bytes",
- "io-uring",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
- "slab",
"socket2",
"tokio-macros",
"windows-sys",
@@ -1215,20 +1293,19 @@ dependencies = [
[[package]]
name = "tokio-boring2"
-version = "5.0.0-alpha.10"
+version = "5.0.0-alpha.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e756617f510b6822897b6fe9c40d643585e524ed11de2e308bff1ee837eefe2"
+checksum = "0f81df1210d791f31d72d840de8fbd80b9c3cb324956523048b1413e2bd55756"
dependencies = [
- "boring-sys2",
"boring2",
"tokio",
]
[[package]]
name = "tokio-macros"
-version = "2.5.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
@@ -1249,9 +1326,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
-version = "0.27.0"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1"
+checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
dependencies = [
"futures-util",
"log",
@@ -1274,9 +1351,9 @@ dependencies = [
[[package]]
name = "tower"
-version = "0.5.2"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
@@ -1289,9 +1366,9 @@ dependencies = [
[[package]]
name = "tower-http"
-version = "0.6.6"
+version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"async-compression",
"bitflags",
@@ -1327,9 +1404,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
-version = "0.27.0"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d"
+checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
dependencies = [
"bytes",
"data-encoding",
@@ -1344,18 +1421,18 @@ dependencies = [
[[package]]
name = "typed-builder"
-version = "0.22.0"
+version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a"
+checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda"
dependencies = [
"typed-builder-macro",
]
[[package]]
name = "typed-builder-macro"
-version = "0.22.0"
+version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221"
+checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26"
dependencies = [
"proc-macro2",
"quote",
@@ -1374,12 +1451,30 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
[[package]]
name = "version_check"
version = "0.9.5"
@@ -1458,77 +1553,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
-version = "0.59.0"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows-targets",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
-dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows-link",
]
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
-
-[[package]]
-name = "windows_i686_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
-
[[package]]
name = "wit-bindgen"
version = "0.46.0"
@@ -1537,9 +1568,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wreq"
-version = "6.0.0-rc.20"
+version = "6.0.0-rc.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "776885d1475f8358bbde83b65967dae6408de22ef9efa96432fb2b24ab054110"
+checksum = "f79937f6c4df65b3f6f78715b9de2977afe9ee3b3436483c7949a24511e25935"
dependencies = [
"ahash",
"boring2",
@@ -1555,14 +1586,12 @@ dependencies = [
"http2",
"httparse",
"ipnet",
- "iri-string",
"libc",
"percent-encoding",
"pin-project-lite",
"schnellru",
"serde",
"serde_json",
- "serde_urlencoded",
"smallvec",
"socket2",
"tokio",
@@ -1571,6 +1600,7 @@ dependencies = [
"tokio-tungstenite",
"tower",
"tower-http",
+ "url",
"want",
"webpki-root-certs",
"zstd",
@@ -1578,14 +1608,46 @@ dependencies = [
[[package]]
name = "wreq-util"
-version = "3.0.0-rc.5"
+version = "3.0.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a38c2ade0a913ec9e1f7f394de8ebdb78d7123e45f015f9d02fed5bdc9db67b"
+checksum = "6c6bbe24d28beb9ceb58b514bd6a613c759d3b706f768b9d2950d5d35b543c04"
dependencies = [
+ "serde",
+ "strum",
+ "strum_macros",
"typed-builder",
"wreq",
]
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
[[package]]
name = "zerocopy"
version = "0.8.27"
@@ -1606,6 +1668,60 @@ dependencies = [
"syn",
]
+[[package]]
+name = "zerofrom"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "zstd"
version = "0.13.3"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 1e87f3c..0977cc6 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -9,11 +9,9 @@ crate-type = ["cdylib"]
[dependencies]
# HTTP client with browser impersonation
-wreq = { version = "6.0.0-rc.20", default-features = false, features = ["gzip", "deflate", "socks", "cookies", "json", "webpki-roots", "ws"] }
-wreq-util = "3.0.0-rc.1"
-
-# WebSocket support
-futures-util = "0.3"
+wreq = { version = "6.0.0-rc.28", default-features = false, features = ["gzip", "brotli", "deflate", "socks", "cookies", "json", "webpki-roots", "ws"] }
+wreq-util = { version = "3.0.0-rc.10", features = ["emulation-rand", "emulation-serde"] }
+strum = "0.27.2"
# Neon for Node.js bindings
neon = { version = "1.0", default-features = false, features = ["napi-6"] }
@@ -29,9 +27,6 @@ thiserror = "1.0"
# Async runtime (if needed)
tokio = { version = "1.0", features = ["full"] }
-# Global state management
-once_cell = "1.20"
-
[profile.release]
opt-level = 3
lto = true
diff --git a/rust/src/emulation/builders.rs b/rust/src/emulation/builders.rs
new file mode 100644
index 0000000..31ec2e3
--- /dev/null
+++ b/rust/src/emulation/builders.rs
@@ -0,0 +1,346 @@
+use crate::emulation::parse::{
+ parse_alpn_protocol, parse_alps_protocol, parse_certificate_compression_algorithm,
+ parse_http2_setting_id, parse_pseudo_id, parse_tls_version,
+};
+use crate::emulation::payload::{
+ CustomHttp1Options, CustomHttp2ExperimentalSetting, CustomHttp2Options, CustomHttp2Priority,
+ CustomTlsOptions,
+};
+use anyhow::{anyhow, bail, Result};
+use std::collections::HashSet;
+use std::time::Duration;
+use wreq::{
+ http1::Http1Options,
+ http2::{
+ ExperimentalSettings, Http2Options, Priorities, Priority, PseudoOrder, Setting, SettingId,
+ SettingsOrder, StreamDependency, StreamId,
+ },
+ tls::{ExtensionType, TlsOptions},
+};
+
+pub fn build_tls_options(options: CustomTlsOptions) -> Result {
+ let mut builder = TlsOptions::builder();
+
+ if let Some(alpn_protocols) = options.alpn_protocols {
+ builder = builder.alpn_protocols(
+ alpn_protocols
+ .into_iter()
+ .map(|protocol| parse_alpn_protocol(&protocol))
+ .collect::>>()?,
+ );
+ }
+
+ if let Some(alps_protocols) = options.alps_protocols {
+ builder = builder.alps_protocols(
+ alps_protocols
+ .into_iter()
+ .map(|protocol| parse_alps_protocol(&protocol))
+ .collect::>>()?,
+ );
+ }
+
+ if let Some(value) = options.alps_use_new_codepoint {
+ builder = builder.alps_use_new_codepoint(value);
+ }
+ if let Some(value) = options.session_ticket {
+ builder = builder.session_ticket(value);
+ }
+ if let Some(value) = options.min_tls_version {
+ builder = builder.min_tls_version(Some(parse_tls_version(&value)?));
+ }
+ if let Some(value) = options.max_tls_version {
+ builder = builder.max_tls_version(Some(parse_tls_version(&value)?));
+ }
+ if let Some(value) = options.pre_shared_key {
+ builder = builder.pre_shared_key(value);
+ }
+ if let Some(value) = options.enable_ech_grease {
+ builder = builder.enable_ech_grease(value);
+ }
+ if let Some(value) = options.permute_extensions {
+ builder = builder.permute_extensions(Some(value));
+ }
+ if let Some(value) = options.grease_enabled {
+ builder = builder.grease_enabled(Some(value));
+ }
+ if let Some(value) = options.enable_ocsp_stapling {
+ builder = builder.enable_ocsp_stapling(value);
+ }
+ if let Some(value) = options.enable_signed_cert_timestamps {
+ builder = builder.enable_signed_cert_timestamps(value);
+ }
+ if let Some(value) = options.record_size_limit {
+ builder = builder.record_size_limit(Some(value));
+ }
+ if let Some(value) = options.psk_skip_session_ticket {
+ builder = builder.psk_skip_session_ticket(value);
+ }
+ if let Some(value) = options.key_shares_limit {
+ builder = builder.key_shares_limit(Some(value));
+ }
+ if let Some(value) = options.psk_dhe_ke {
+ builder = builder.psk_dhe_ke(value);
+ }
+ if let Some(value) = options.renegotiation {
+ builder = builder.renegotiation(value);
+ }
+ if let Some(value) = options.delegated_credentials {
+ builder = builder.delegated_credentials(value);
+ }
+ if let Some(value) = options.curves_list {
+ builder = builder.curves_list(value);
+ }
+ if let Some(value) = options.cipher_list {
+ builder = builder.cipher_list(value);
+ }
+ if let Some(value) = options.sigalgs_list {
+ builder = builder.sigalgs_list(value);
+ }
+ if let Some(value) = options.certificate_compression_algorithms {
+ builder = builder.certificate_compression_algorithms(
+ value
+ .into_iter()
+ .map(|algorithm| parse_certificate_compression_algorithm(&algorithm))
+ .collect::>>()?,
+ );
+ }
+ if let Some(value) = options.extension_permutation {
+ builder = builder.extension_permutation(
+ value
+ .into_iter()
+ .map(ExtensionType::from)
+ .collect::>(),
+ );
+ }
+ if let Some(value) = options.aes_hw_override {
+ builder = builder.aes_hw_override(Some(value));
+ }
+ if let Some(value) = options.preserve_tls13_cipher_list {
+ builder = builder.preserve_tls13_cipher_list(Some(value));
+ }
+ if let Some(value) = options.random_aes_hw_override {
+ builder = builder.random_aes_hw_override(value);
+ }
+
+ Ok(builder.build())
+}
+
+pub fn build_http1_options(options: CustomHttp1Options) -> Result {
+ let mut builder = Http1Options::builder();
+
+ if let Some(value) = options.http09_responses {
+ builder = builder.http09_responses(value);
+ }
+ if let Some(value) = options.writev {
+ builder = builder.writev(Some(value));
+ }
+ if let Some(value) = options.max_headers {
+ builder = builder.max_headers(value);
+ }
+ if let Some(value) = options.read_buf_exact_size {
+ builder = builder.read_buf_exact_size(Some(value));
+ }
+ if let Some(value) = options.max_buf_size {
+ if value < 8192 {
+ bail!("Invalid emulation http1Options.maxBufSize: must be at least 8192");
+ }
+ builder = builder.max_buf_size(value);
+ }
+ if options.read_buf_exact_size.is_some() && options.max_buf_size.is_some() {
+ bail!("Invalid emulation http1Options: readBufExactSize and maxBufSize cannot both be set");
+ }
+ if let Some(value) = options.ignore_invalid_headers_in_responses {
+ builder = builder.ignore_invalid_headers_in_responses(value);
+ }
+ if let Some(value) = options.allow_spaces_after_header_name_in_responses {
+ builder = builder.allow_spaces_after_header_name_in_responses(value);
+ }
+ if let Some(value) = options.allow_obsolete_multiline_headers_in_responses {
+ builder = builder.allow_obsolete_multiline_headers_in_responses(value);
+ }
+
+ Ok(builder.build())
+}
+
+pub fn build_http2_options(options: CustomHttp2Options) -> Result {
+ let mut builder = Http2Options::builder();
+
+ if let Some(value) = options.adaptive_window {
+ builder = builder.adaptive_window(value);
+ }
+ if let Some(value) = options.initial_stream_id {
+ builder = builder.initial_stream_id(Some(value));
+ }
+ if let Some(value) = options.initial_connection_window_size {
+ builder = builder.initial_connection_window_size(Some(value));
+ }
+ if let Some(value) = options.initial_window_size {
+ builder = builder.initial_window_size(Some(value));
+ }
+ if let Some(value) = options.initial_max_send_streams {
+ builder = builder.initial_max_send_streams(Some(value));
+ }
+ if let Some(value) = options.max_frame_size {
+ builder = builder.max_frame_size(Some(value));
+ }
+ if let Some(value) = options.keep_alive_interval {
+ builder = builder.keep_alive_interval(Some(Duration::from_millis(value)));
+ }
+ if let Some(value) = options.keep_alive_timeout {
+ builder = builder.keep_alive_timeout(Duration::from_millis(value));
+ }
+ if let Some(value) = options.keep_alive_while_idle {
+ builder = builder.keep_alive_while_idle(value);
+ }
+ if let Some(value) = options.max_concurrent_reset_streams {
+ builder = builder.max_concurrent_reset_streams(value);
+ }
+ if let Some(value) = options.max_send_buffer_size {
+ builder = builder.max_send_buf_size(value);
+ }
+ if let Some(value) = options.max_concurrent_streams {
+ builder = builder.max_concurrent_streams(Some(value));
+ }
+ if let Some(value) = options.max_header_list_size {
+ builder = builder.max_header_list_size(value);
+ }
+ if let Some(value) = options.max_pending_accept_reset_streams {
+ builder = builder.max_pending_accept_reset_streams(Some(value));
+ }
+ if let Some(value) = options.enable_push {
+ builder = builder.enable_push(value);
+ }
+ if let Some(value) = options.header_table_size {
+ builder = builder.header_table_size(Some(value));
+ }
+ if let Some(value) = options.enable_connect_protocol {
+ builder = builder.enable_connect_protocol(value);
+ }
+ if let Some(value) = options.no_rfc7540_priorities {
+ builder = builder.no_rfc7540_priorities(value);
+ }
+ if let Some(settings_order) = options.settings_order {
+ builder = builder.settings_order(Some(build_settings_order(settings_order)?));
+ }
+ if let Some(pseudo_order) = options.headers_pseudo_order {
+ builder = builder.headers_pseudo_order(Some(build_pseudo_order(pseudo_order)?));
+ }
+ if let Some(dep) = options.headers_stream_dependency {
+ builder = builder.headers_stream_dependency(Some(StreamDependency::new(
+ StreamId::from(dep.dependency_id),
+ dep.weight,
+ dep.exclusive,
+ )));
+ }
+ if let Some(priorities) = options.priorities {
+ builder = builder.priorities(Some(build_priorities(priorities)?));
+ }
+ if let Some(experimental_settings) = options.experimental_settings {
+ builder = builder
+ .experimental_settings(Some(build_experimental_settings(experimental_settings)?));
+ }
+
+ Ok(builder.build())
+}
+
+fn build_pseudo_order(pseudo_order: Vec) -> Result {
+ let mut builder = PseudoOrder::builder();
+ let mut seen = HashSet::with_capacity(pseudo_order.len());
+
+ for pseudo_id in &pseudo_order {
+ let id = parse_pseudo_id(pseudo_id)?;
+ if !seen.insert(pseudo_id.clone()) {
+ bail!("Duplicate emulation http2Options.headersPseudoOrder entry: {pseudo_id}");
+ }
+ builder = builder.push(id);
+ }
+
+ Ok(builder.build())
+}
+
+fn build_settings_order(settings_order: Vec) -> Result {
+ let mut builder = SettingsOrder::builder();
+ let mut seen = HashSet::with_capacity(settings_order.len());
+
+ for setting in settings_order {
+ let setting_id = parse_http2_setting_id(&setting)?;
+ if !seen.insert(setting_id.clone()) {
+ bail!("Duplicate emulation http2Options.settingsOrder entry: {setting}");
+ }
+ builder = builder.push(setting_id);
+ }
+
+ Ok(builder.build())
+}
+
+fn build_priorities(priorities: Vec) -> Result {
+ let mut builder = Priorities::builder();
+ let mut seen_stream_ids = HashSet::with_capacity(priorities.len());
+
+ for priority in priorities {
+ if priority.stream_id == 0 {
+ bail!(
+ "Invalid emulation http2Options.priorities entry: streamId must be greater than 0"
+ );
+ }
+ if !seen_stream_ids.insert(priority.stream_id) {
+ bail!(
+ "Duplicate emulation http2Options.priorities streamId: {}",
+ priority.stream_id
+ );
+ }
+
+ let dependency = StreamDependency::new(
+ StreamId::from(priority.dependency.dependency_id),
+ priority.dependency.weight,
+ priority.dependency.exclusive,
+ );
+
+ builder = builder.push(Priority::new(
+ StreamId::from(priority.stream_id),
+ dependency,
+ ));
+ }
+
+ Ok(builder.build())
+}
+
+fn build_experimental_settings(
+ experimental_settings: Vec,
+) -> Result {
+ let mut builder = ExperimentalSettings::builder();
+ let mut seen_ids = HashSet::with_capacity(experimental_settings.len());
+ let max_id = 15u16;
+
+ for setting in experimental_settings {
+ if setting.id == 0 || setting.id > max_id {
+ bail!(
+ "Invalid emulation http2Options.experimentalSettings entry: id must be between 1 and {}",
+ max_id
+ );
+ }
+ if !matches!(SettingId::from(setting.id), SettingId::Unknown(_)) {
+ bail!(
+ "Invalid emulation http2Options.experimentalSettings entry: {} is a standard HTTP/2 setting id",
+ setting.id
+ );
+ }
+ if !seen_ids.insert(setting.id) {
+ bail!(
+ "Duplicate emulation http2Options.experimentalSettings id: {}",
+ setting.id
+ );
+ }
+
+ let setting =
+ Setting::from_id(SettingId::Unknown(setting.id), setting.value).ok_or_else(|| {
+ anyhow!(
+ "Invalid emulation http2Options.experimentalSettings id: {}",
+ setting.id
+ )
+ })?;
+ builder = builder.push(setting);
+ }
+
+ Ok(builder.build())
+}
diff --git a/rust/src/emulation/mod.rs b/rust/src/emulation/mod.rs
new file mode 100644
index 0000000..9c41762
--- /dev/null
+++ b/rust/src/emulation/mod.rs
@@ -0,0 +1,6 @@
+mod builders;
+mod parse;
+mod payload;
+mod resolve;
+
+pub use resolve::resolve_emulation;
diff --git a/rust/src/emulation/parse.rs b/rust/src/emulation/parse.rs
new file mode 100644
index 0000000..b72fd1d
--- /dev/null
+++ b/rust/src/emulation/parse.rs
@@ -0,0 +1,69 @@
+use anyhow::{bail, Result};
+use wreq::{
+ http2::{PseudoId, SettingId},
+ tls::{AlpnProtocol, AlpsProtocol, CertificateCompressionAlgorithm, TlsVersion},
+};
+
+pub fn parse_tls_version(value: &str) -> Result {
+ match value {
+ "1.0" | "TLS1.0" => Ok(TlsVersion::TLS_1_0),
+ "1.1" | "TLS1.1" => Ok(TlsVersion::TLS_1_1),
+ "1.2" | "TLS1.2" => Ok(TlsVersion::TLS_1_2),
+ "1.3" | "TLS1.3" => Ok(TlsVersion::TLS_1_3),
+ other => bail!("Invalid TLS version: {other}"),
+ }
+}
+
+pub fn parse_alpn_protocol(value: &str) -> Result {
+ match value {
+ "HTTP1" => Ok(AlpnProtocol::HTTP1),
+ "HTTP2" => Ok(AlpnProtocol::HTTP2),
+ "HTTP3" => Ok(AlpnProtocol::HTTP3),
+ other => bail!("Invalid ALPN protocol: {other}"),
+ }
+}
+
+pub fn parse_alps_protocol(value: &str) -> Result {
+ match value {
+ "HTTP1" => Ok(AlpsProtocol::HTTP1),
+ "HTTP2" => Ok(AlpsProtocol::HTTP2),
+ "HTTP3" => Ok(AlpsProtocol::HTTP3),
+ other => bail!("Invalid ALPS protocol: {other}"),
+ }
+}
+
+pub fn parse_certificate_compression_algorithm(
+ value: &str,
+) -> Result {
+ match value {
+ "zlib" => Ok(CertificateCompressionAlgorithm::ZLIB),
+ "brotli" => Ok(CertificateCompressionAlgorithm::BROTLI),
+ "zstd" => Ok(CertificateCompressionAlgorithm::ZSTD),
+ other => bail!("Invalid certificate compression algorithm: {other}"),
+ }
+}
+
+pub fn parse_pseudo_id(value: &str) -> Result {
+ match value {
+ "Method" => Ok(PseudoId::Method),
+ "Scheme" => Ok(PseudoId::Scheme),
+ "Authority" => Ok(PseudoId::Authority),
+ "Path" => Ok(PseudoId::Path),
+ "Protocol" => Ok(PseudoId::Protocol),
+ other => bail!("Invalid HTTP/2 pseudo-header id: {other}"),
+ }
+}
+
+pub fn parse_http2_setting_id(value: &str) -> Result {
+ match value {
+ "HeaderTableSize" => Ok(SettingId::HeaderTableSize),
+ "EnablePush" => Ok(SettingId::EnablePush),
+ "MaxConcurrentStreams" => Ok(SettingId::MaxConcurrentStreams),
+ "InitialWindowSize" => Ok(SettingId::InitialWindowSize),
+ "MaxFrameSize" => Ok(SettingId::MaxFrameSize),
+ "MaxHeaderListSize" => Ok(SettingId::MaxHeaderListSize),
+ "EnableConnectProtocol" => Ok(SettingId::EnableConnectProtocol),
+ "NoRfc7540Priorities" => Ok(SettingId::NoRfc7540Priorities),
+ other => bail!("Invalid HTTP/2 setting id: {other}"),
+ }
+}
diff --git a/rust/src/emulation/payload.rs b/rust/src/emulation/payload.rs
new file mode 100644
index 0000000..1029def
--- /dev/null
+++ b/rust/src/emulation/payload.rs
@@ -0,0 +1,163 @@
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomEmulationPayload {
+ #[serde(default)]
+ pub tls_options: Option,
+ #[serde(default)]
+ pub http1_options: Option,
+ #[serde(default)]
+ pub http2_options: Option,
+}
+
+#[derive(Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomTlsOptions {
+ #[serde(default)]
+ pub alpn_protocols: Option>,
+ #[serde(default)]
+ pub alps_protocols: Option>,
+ #[serde(default)]
+ pub alps_use_new_codepoint: Option,
+ #[serde(default)]
+ pub session_ticket: Option,
+ #[serde(default)]
+ pub min_tls_version: Option,
+ #[serde(default)]
+ pub max_tls_version: Option,
+ #[serde(default)]
+ pub pre_shared_key: Option,
+ #[serde(default)]
+ pub enable_ech_grease: Option,
+ #[serde(default)]
+ pub permute_extensions: Option,
+ #[serde(default)]
+ pub grease_enabled: Option,
+ #[serde(default)]
+ pub enable_ocsp_stapling: Option,
+ #[serde(default)]
+ pub enable_signed_cert_timestamps: Option,
+ #[serde(default)]
+ pub record_size_limit: Option,
+ #[serde(default)]
+ pub psk_skip_session_ticket: Option,
+ #[serde(default)]
+ pub key_shares_limit: Option,
+ #[serde(default)]
+ pub psk_dhe_ke: Option,
+ #[serde(default)]
+ pub renegotiation: Option,
+ #[serde(default)]
+ pub delegated_credentials: Option,
+ #[serde(default)]
+ pub curves_list: Option,
+ #[serde(default)]
+ pub cipher_list: Option,
+ #[serde(default)]
+ pub sigalgs_list: Option,
+ #[serde(default)]
+ pub certificate_compression_algorithms: Option>,
+ #[serde(default)]
+ pub extension_permutation: Option>,
+ #[serde(default)]
+ pub aes_hw_override: Option,
+ #[serde(default)]
+ pub preserve_tls13_cipher_list: Option,
+ #[serde(default)]
+ pub random_aes_hw_override: Option,
+}
+
+#[derive(Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomHttp1Options {
+ #[serde(default)]
+ pub http09_responses: Option,
+ #[serde(default)]
+ pub writev: Option,
+ #[serde(default)]
+ pub max_headers: Option,
+ #[serde(default)]
+ pub read_buf_exact_size: Option,
+ #[serde(default)]
+ pub max_buf_size: Option,
+ #[serde(default)]
+ pub ignore_invalid_headers_in_responses: Option,
+ #[serde(default)]
+ pub allow_spaces_after_header_name_in_responses: Option,
+ #[serde(default)]
+ pub allow_obsolete_multiline_headers_in_responses: Option,
+}
+
+#[derive(Debug, Deserialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomHttp2Options {
+ #[serde(default)]
+ pub adaptive_window: Option,
+ #[serde(default)]
+ pub initial_stream_id: Option,
+ #[serde(default)]
+ pub initial_connection_window_size: Option,
+ #[serde(default)]
+ pub initial_window_size: Option,
+ #[serde(default)]
+ pub initial_max_send_streams: Option,
+ #[serde(default)]
+ pub max_frame_size: Option,
+ #[serde(default)]
+ pub keep_alive_interval: Option,
+ #[serde(default)]
+ pub keep_alive_timeout: Option,
+ #[serde(default)]
+ pub keep_alive_while_idle: Option,
+ #[serde(default)]
+ pub max_concurrent_reset_streams: Option,
+ #[serde(default)]
+ pub max_send_buffer_size: Option,
+ #[serde(default)]
+ pub max_concurrent_streams: Option,
+ #[serde(default)]
+ pub max_header_list_size: Option,
+ #[serde(default)]
+ pub max_pending_accept_reset_streams: Option,
+ #[serde(default)]
+ pub enable_push: Option,
+ #[serde(default)]
+ pub header_table_size: Option,
+ #[serde(default)]
+ pub enable_connect_protocol: Option,
+ #[serde(default)]
+ pub no_rfc7540_priorities: Option,
+ #[serde(default)]
+ pub settings_order: Option>,
+ #[serde(default)]
+ pub headers_pseudo_order: Option>,
+ #[serde(default)]
+ pub headers_stream_dependency: Option,
+ #[serde(default)]
+ pub priorities: Option>,
+ #[serde(default)]
+ pub experimental_settings: Option>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomHttp2Priority {
+ pub stream_id: u32,
+ pub dependency: CustomHttp2StreamDependency,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomHttp2StreamDependency {
+ pub dependency_id: u32,
+ pub weight: u8,
+ #[serde(default)]
+ pub exclusive: bool,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct CustomHttp2ExperimentalSetting {
+ pub id: u16,
+ pub value: u32,
+}
diff --git a/rust/src/emulation/resolve.rs b/rust/src/emulation/resolve.rs
new file mode 100644
index 0000000..3e57ca7
--- /dev/null
+++ b/rust/src/emulation/resolve.rs
@@ -0,0 +1,39 @@
+use crate::emulation::builders::{build_http1_options, build_http2_options, build_tls_options};
+use crate::emulation::payload::CustomEmulationPayload;
+use anyhow::{Context, Result};
+use wreq::{Emulation as WreqEmulation, EmulationFactory};
+use wreq_util::Emulation as BrowserEmulation;
+
+pub fn resolve_emulation(
+ browser: BrowserEmulation,
+ emulation_json: Option<&str>,
+) -> Result {
+ let mut emulation = browser.emulation();
+
+ if let Some(emulation_json) = emulation_json {
+ let payload = parse_payload(emulation_json)?;
+ apply_payload(&mut emulation, payload)?;
+ }
+
+ Ok(emulation)
+}
+
+fn parse_payload(emulation_json: &str) -> Result {
+ serde_json::from_str(emulation_json).context("Failed to parse emulation JSON")
+}
+
+fn apply_payload(emulation: &mut WreqEmulation, payload: CustomEmulationPayload) -> Result<()> {
+ if let Some(tls_options) = payload.tls_options {
+ *emulation.tls_options_mut() = Some(build_tls_options(tls_options)?);
+ }
+
+ if let Some(http1_options) = payload.http1_options {
+ *emulation.http1_options_mut() = Some(build_http1_options(http1_options)?);
+ }
+
+ if let Some(http2_options) = payload.http2_options {
+ *emulation.http2_options_mut() = Some(build_http2_options(http2_options)?);
+ }
+
+ Ok(())
+}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index f794f25..bf681ad 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -1,547 +1,11 @@
-mod client;
-mod websocket;
+mod emulation;
+mod napi;
+mod store;
+mod transport;
use neon::prelude::*;
-use neon::types::buffer::TypedArray;
-use client::{make_request, RequestOptions, Response};
-use websocket::{connect_websocket, store_connection, get_connection, remove_connection, WebSocketOptions, WS_RUNTIME};
-use std::collections::HashMap;
-use std::sync::Arc;
-use wreq_util::Emulation;
-use futures_util::StreamExt;
-use wreq::ws::message::Message;
-// Parse browser string to Emulation enum
-fn parse_emulation(browser: &str) -> Emulation {
- match browser {
- // Chrome
- "chrome_100" => Emulation::Chrome100,
- "chrome_101" => Emulation::Chrome101,
- "chrome_104" => Emulation::Chrome104,
- "chrome_105" => Emulation::Chrome105,
- "chrome_106" => Emulation::Chrome106,
- "chrome_107" => Emulation::Chrome107,
- "chrome_108" => Emulation::Chrome108,
- "chrome_109" => Emulation::Chrome109,
- "chrome_110" => Emulation::Chrome110,
- "chrome_114" => Emulation::Chrome114,
- "chrome_116" => Emulation::Chrome116,
- "chrome_117" => Emulation::Chrome117,
- "chrome_118" => Emulation::Chrome118,
- "chrome_119" => Emulation::Chrome119,
- "chrome_120" => Emulation::Chrome120,
- "chrome_123" => Emulation::Chrome123,
- "chrome_124" => Emulation::Chrome124,
- "chrome_126" => Emulation::Chrome126,
- "chrome_127" => Emulation::Chrome127,
- "chrome_128" => Emulation::Chrome128,
- "chrome_129" => Emulation::Chrome129,
- "chrome_130" => Emulation::Chrome130,
- "chrome_131" => Emulation::Chrome131,
- "chrome_132" => Emulation::Chrome132,
- "chrome_133" => Emulation::Chrome133,
- "chrome_134" => Emulation::Chrome134,
- "chrome_135" => Emulation::Chrome135,
- "chrome_136" => Emulation::Chrome136,
- "chrome_137" => Emulation::Chrome137,
- // Edge
- "edge_101" => Emulation::Edge101,
- "edge_122" => Emulation::Edge122,
- "edge_127" => Emulation::Edge127,
- "edge_131" => Emulation::Edge131,
- "edge_134" => Emulation::Edge134,
- // Safari
- "safari_ios_17_2" => Emulation::SafariIos17_2,
- "safari_ios_17_4_1" => Emulation::SafariIos17_4_1,
- "safari_ios_16_5" => Emulation::SafariIos16_5,
- "safari_15_3" => Emulation::Safari15_3,
- "safari_15_5" => Emulation::Safari15_5,
- "safari_15_6_1" => Emulation::Safari15_6_1,
- "safari_16" => Emulation::Safari16,
- "safari_16_5" => Emulation::Safari16_5,
- "safari_17_0" => Emulation::Safari17_0,
- "safari_17_2_1" => Emulation::Safari17_2_1,
- "safari_17_4_1" => Emulation::Safari17_4_1,
- "safari_17_5" => Emulation::Safari17_5,
- "safari_18" => Emulation::Safari18,
- "safari_ipad_18" => Emulation::SafariIPad18,
- "safari_18_2" => Emulation::Safari18_2,
- "safari_ios_18_1_1" => Emulation::SafariIos18_1_1,
- "safari_18_3" => Emulation::Safari18_3,
- "safari_18_3_1" => Emulation::Safari18_3_1,
- "safari_18_5" => Emulation::Safari18_5,
- // Firefox
- "firefox_109" => Emulation::Firefox109,
- "firefox_117" => Emulation::Firefox117,
- "firefox_128" => Emulation::Firefox128,
- "firefox_133" => Emulation::Firefox133,
- "firefox_135" => Emulation::Firefox135,
- "firefox_private_135" => Emulation::FirefoxPrivate135,
- "firefox_android_135" => Emulation::FirefoxAndroid135,
- "firefox_136" => Emulation::Firefox136,
- "firefox_private_136" => Emulation::FirefoxPrivate136,
- "firefox_139" => Emulation::Firefox139,
- // Opera
- "opera_116" => Emulation::Opera116,
- "opera_117" => Emulation::Opera117,
- "opera_118" => Emulation::Opera118,
- "opera_119" => Emulation::Opera119,
- // OkHttp
- "okhttp_3_9" => Emulation::OkHttp3_9,
- "okhttp_3_11" => Emulation::OkHttp3_11,
- "okhttp_3_13" => Emulation::OkHttp3_13,
- "okhttp_3_14" => Emulation::OkHttp3_14,
- "okhttp_4_9" => Emulation::OkHttp4_9,
- "okhttp_4_10" => Emulation::OkHttp4_10,
- "okhttp_4_12" => Emulation::OkHttp4_12,
- "okhttp_5" => Emulation::OkHttp5,
- // Default to Chrome 137
- _ => Emulation::Chrome137,
- }
-}
-
-// Convert JS object to RequestOptions
-fn js_object_to_request_options(cx: &mut FunctionContext, obj: Handle) -> NeonResult {
- // Get URL (required)
- let url: Handle = obj.get(cx, "url")?;
- let url = url.value(cx);
-
- // Get browser (optional, defaults to chrome_137)
- let browser_str = obj
- .get_opt(cx, "browser")?
- .and_then(|v: Handle| v.downcast::(cx).ok())
- .map(|v| v.value(cx))
- .unwrap_or_else(|| "chrome_137".to_string());
-
- let emulation = parse_emulation(&browser_str);
-
- // Get method (optional, defaults to GET)
- let method = obj
- .get_opt(cx, "method")?
- .and_then(|v: Handle| v.downcast::(cx).ok())
- .map(|v| v.value(cx))
- .unwrap_or_else(|| "GET".to_string());
-
- // Get headers (optional)
- let mut headers = HashMap::new();
- if let Ok(Some(headers_obj)) = obj.get_opt::(cx, "headers") {
- let keys = headers_obj.get_own_property_names(cx)?;
- let keys_vec = keys.to_vec(cx)?;
-
- for key_val in keys_vec {
- if let Ok(key_str) = key_val.downcast::(cx) {
- let key = key_str.value(cx);
- if let Ok(value) = headers_obj.get::(cx, key.as_str()) {
- headers.insert(key, value.value(cx));
- }
- }
- }
- }
-
- // Get body (optional)
- let body = obj
- .get_opt(cx, "body")?
- .and_then(|v: Handle| v.downcast::(cx).ok())
- .map(|v| v.value(cx));
-
- // Get proxy (optional)
- let proxy = obj
- .get_opt(cx, "proxy")?
- .and_then(|v: Handle| v.downcast::(cx).ok())
- .map(|v| v.value(cx));
-
- // Get timeout (optional, defaults to 30000ms)
- let timeout = obj
- .get_opt(cx, "timeout")?
- .and_then(|v: Handle| v.downcast::(cx).ok())
- .map(|v| v.value(cx) as u64)
- .unwrap_or(30000);
-
- Ok(RequestOptions {
- url,
- emulation,
- headers,
- method,
- body,
- proxy,
- timeout,
- })
-}
-
-// Convert Response to JS object
-fn response_to_js_object<'a, C: Context<'a>>(cx: &mut C, response: Response) -> JsResult<'a, JsObject> {
- let obj = cx.empty_object();
-
- // Status
- let status = cx.number(response.status as f64);
- obj.set(cx, "status", status)?;
-
- // URL
- let url = cx.string(&response.url);
- obj.set(cx, "url", url)?;
-
- // Headers
- let headers_obj = cx.empty_object();
- for (key, value) in response.headers {
- let value_str = cx.string(&value);
- headers_obj.set(cx, key.as_str(), value_str)?;
- }
- obj.set(cx, "headers", headers_obj)?;
-
- // Cookies
- let cookies_obj = cx.empty_object();
- for (key, value) in response.cookies {
- let value_str = cx.string(&value);
- cookies_obj.set(cx, key.as_str(), value_str)?;
- }
- obj.set(cx, "cookies", cookies_obj)?;
-
- // Body
- let body = cx.string(&response.body);
- obj.set(cx, "body", body)?;
-
- Ok(obj)
-}
-
-// Main request function exported to Node.js
-fn request(mut cx: FunctionContext) -> JsResult {
- // Get the options object
- let options_obj = cx.argument::(0)?;
-
- // Convert JS object to Rust struct
- let options = js_object_to_request_options(&mut cx, options_obj)?;
-
- // Create a promise
- let channel = cx.channel();
- let (deferred, promise) = cx.promise();
-
- // Create a new Tokio runtime for this request
- std::thread::spawn(move || {
- let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
-
- // Make the request
- let result = rt.block_on(make_request(options));
-
- // Send result back to JS
- deferred.settle_with(&channel, move |mut cx| {
- match result {
- Ok(response) => response_to_js_object(&mut cx, response),
- Err(e) => {
- // Format error with full chain for better debugging
- let error_msg = format!("{:#}", e);
- cx.throw_error(error_msg)
- }
- }
- });
- });
-
- Ok(promise)
-}
-
-// Get list of available browser profiles
-fn get_profiles(mut cx: FunctionContext) -> JsResult {
- let profiles = vec![
- // Chrome
- "chrome_100", "chrome_101", "chrome_104", "chrome_105", "chrome_106", "chrome_107",
- "chrome_108", "chrome_109", "chrome_110", "chrome_114", "chrome_116", "chrome_117",
- "chrome_118", "chrome_119", "chrome_120", "chrome_123", "chrome_124", "chrome_126",
- "chrome_127", "chrome_128", "chrome_129", "chrome_130", "chrome_131", "chrome_132",
- "chrome_133", "chrome_134", "chrome_135", "chrome_136", "chrome_137",
- // Edge
- "edge_101", "edge_122", "edge_127", "edge_131", "edge_134",
- // Safari
- "safari_ios_17_2", "safari_ios_17_4_1", "safari_ios_16_5",
- "safari_15_3", "safari_15_5", "safari_15_6_1", "safari_16", "safari_16_5",
- "safari_17_0", "safari_17_2_1", "safari_17_4_1", "safari_17_5", "safari_18",
- "safari_ipad_18", "safari_18_2", "safari_ios_18_1_1",
- "safari_18_3", "safari_18_3_1", "safari_18_5",
- // Firefox
- "firefox_109", "firefox_117", "firefox_128", "firefox_133", "firefox_135",
- "firefox_private_135", "firefox_android_135",
- "firefox_136", "firefox_private_136", "firefox_139",
- // Opera
- "opera_116", "opera_117", "opera_118", "opera_119",
- // OkHttp
- "okhttp_3_9", "okhttp_3_11", "okhttp_3_13", "okhttp_3_14",
- "okhttp_4_9", "okhttp_4_10", "okhttp_4_12", "okhttp_5",
- ];
-
- let js_array = cx.empty_array();
-
- for (i, profile) in profiles.iter().enumerate() {
- let js_string = cx.string(*profile);
- js_array.set(&mut cx, i as u32, js_string)?;
- }
-
- Ok(js_array)
-}
-
-// WebSocket connection function
-fn websocket_connect(mut cx: FunctionContext) -> JsResult {
- // Get the options object
- let options_obj = cx.argument::(0)?;
-
- // Get URL (required)
- let url: Handle = options_obj.get(&mut cx, "url")?;
- let url = url.value(&mut cx);
-
- // Get browser (optional, defaults to chrome_137)
- let browser_str = options_obj
- .get_opt(&mut cx, "browser")?
- .and_then(|v: Handle| v.downcast::(&mut cx).ok())
- .map(|v| v.value(&mut cx))
- .unwrap_or_else(|| "chrome_137".to_string());
-
- let emulation = parse_emulation(&browser_str);
-
- // Get headers (optional)
- let mut headers = HashMap::new();
- if let Ok(Some(headers_obj)) = options_obj.get_opt::(&mut cx, "headers") {
- let keys = headers_obj.get_own_property_names(&mut cx)?;
- let keys_vec = keys.to_vec(&mut cx)?;
-
- for key_val in keys_vec {
- if let Ok(key_str) = key_val.downcast::(&mut cx) {
- let key = key_str.value(&mut cx);
- if let Ok(value) = headers_obj.get::(&mut cx, key.as_str()) {
- headers.insert(key, value.value(&mut cx));
- }
- }
- }
- }
-
- // Get proxy (optional)
- let proxy = options_obj
- .get_opt(&mut cx, "proxy")?
- .and_then(|v: Handle| v.downcast::(&mut cx).ok())
- .map(|v| v.value(&mut cx));
-
- // Get callbacks
- let on_message: Handle = options_obj.get(&mut cx, "onMessage")?;
- let on_close_opt = options_obj
- .get_opt::(&mut cx, "onClose")?;
- let on_error_opt = options_obj
- .get_opt::(&mut cx, "onError")?;
-
- let options = WebSocketOptions {
- url,
- emulation,
- headers,
- proxy,
- };
-
- // Create a promise
- let channel = cx.channel();
- let (deferred, promise) = cx.promise();
-
- // Keep callbacks alive
- let on_message = Arc::new(on_message.root(&mut cx));
- let on_close = on_close_opt.map(|f| Arc::new(f.root(&mut cx)));
- let on_error = on_error_opt.map(|f| Arc::new(f.root(&mut cx)));
-
- // Spawn async task
- std::thread::spawn(move || {
- let result: Result<_, anyhow::Error> = WS_RUNTIME.block_on(async {
- // Connect to WebSocket
- let (connection, mut receiver) = connect_websocket(options).await?;
-
- // Start message receiver loop
- let channel_clone = channel.clone();
- let on_message_clone = on_message.clone();
- let on_close_clone = on_close.clone();
- let on_error_clone = on_error.clone();
-
- tokio::spawn(async move {
- while let Some(msg_result) = receiver.next().await {
- match msg_result {
- Ok(msg) => {
- match msg {
- Message::Text(text) => {
- let text = text.to_string();
- let on_message_ref = on_message_clone.clone();
- channel_clone.send(move |mut cx| {
- let cb = on_message_ref.to_inner(&mut cx);
- let this = cx.undefined();
- let args = vec![cx.string(text).upcast()];
- cb.call(&mut cx, this, args)?;
- Ok(())
- });
- }
- Message::Binary(data) => {
- let data = data.to_vec();
- let on_message_ref = on_message_clone.clone();
- channel_clone.send(move |mut cx| {
- let cb = on_message_ref.to_inner(&mut cx);
- let this = cx.undefined();
- let mut buffer = cx.buffer(data.len())?;
- buffer.as_mut_slice(&mut cx).copy_from_slice(&data);
- let args = vec![buffer.upcast()];
- cb.call(&mut cx, this, args)?;
- Ok(())
- });
- }
- Message::Close(_) => {
- if let Some(on_close_ref) = on_close_clone.as_ref() {
- let on_close_ref = on_close_ref.clone();
- channel_clone.send(move |mut cx| {
- let cb = on_close_ref.to_inner(&mut cx);
- let this = cx.undefined();
- cb.call(&mut cx, this, vec![])?;
- Ok(())
- });
- }
- break;
- }
- _ => {
- // Ignore Ping/Pong messages
- }
- }
- }
- Err(e) => {
- if let Some(on_error_ref) = on_error_clone.as_ref() {
- let error_msg = format!("{:#}", e);
- let on_error_ref = on_error_ref.clone();
- channel_clone.send(move |mut cx| {
- let cb = on_error_ref.to_inner(&mut cx);
- let this = cx.undefined();
- let args = vec![cx.string(error_msg).upcast()];
- cb.call(&mut cx, this, args)?;
- Ok(())
- });
- }
- break;
- }
- }
- }
- });
-
- Ok(connection)
- });
-
- // Send result back to JS
- deferred.settle_with(&channel, move |mut cx| {
- match result {
- Ok(connection) => {
- // Store connection and get ID
- let id = store_connection(connection);
-
- let obj = cx.empty_object();
- let id_num = cx.number(id as f64);
- obj.set(&mut cx, "_id", id_num)?;
- Ok(obj)
- }
- Err(e) => {
- let error_msg = format!("{:#}", e);
- cx.throw_error(error_msg)
- }
- }
- });
- });
-
- Ok(promise)
-}
-
-// WebSocket send function
-fn websocket_send(mut cx: FunctionContext) -> JsResult {
- let ws_obj = cx.argument::(0)?;
- let data = cx.argument::(1)?;
-
- // Get the connection ID from the object
- let id_val: Handle = ws_obj.get(&mut cx, "_id")?;
- let id = id_val.value(&mut cx) as u64;
-
- // Get connection from global storage
- let connection = match get_connection(id) {
- Some(conn) => conn,
- None => return cx.throw_error("WebSocket connection not found"),
- };
-
- let channel = cx.channel();
- let (deferred, promise) = cx.promise();
-
- // Check if data is string or buffer
- let is_text = data.is_a::(&mut cx);
- let send_data = if is_text {
- let text = data.downcast_or_throw::(&mut cx)?;
- SendData::Text(text.value(&mut cx))
- } else if let Ok(buffer) = data.downcast::(&mut cx) {
- let data = buffer.as_slice(&cx).to_vec();
- SendData::Binary(data)
- } else {
- return cx.throw_error("Data must be a string or Buffer");
- };
-
- std::thread::spawn(move || {
- let result = WS_RUNTIME.block_on(async {
- match send_data {
- SendData::Text(text) => connection.send_text(text).await,
- SendData::Binary(data) => connection.send_binary(data).await,
- }
- });
-
- deferred.settle_with(&channel, move |mut cx| {
- match result {
- Ok(()) => Ok(cx.undefined()),
- Err(e) => {
- let error_msg = format!("{:#}", e);
- cx.throw_error(error_msg)
- }
- }
- });
- });
-
- Ok(promise)
-}
-
-enum SendData {
- Text(String),
- Binary(Vec),
-}
-
-// WebSocket close function
-fn websocket_close(mut cx: FunctionContext) -> JsResult {
- let ws_obj = cx.argument::(0)?;
-
- // Get the connection ID from the object
- let id_val: Handle = ws_obj.get(&mut cx, "_id")?;
- let id = id_val.value(&mut cx) as u64;
-
- // Get connection from global storage
- let connection = match get_connection(id) {
- Some(conn) => conn,
- None => return cx.throw_error("WebSocket connection not found"),
- };
-
- let channel = cx.channel();
- let (deferred, promise) = cx.promise();
-
- std::thread::spawn(move || {
- let result = WS_RUNTIME.block_on(connection.close());
-
- // Remove connection from storage after closing
- remove_connection(id);
-
- deferred.settle_with(&channel, move |mut cx| {
- match result {
- Ok(()) => Ok(cx.undefined()),
- Err(e) => {
- let error_msg = format!("{:#}", e);
- cx.throw_error(error_msg)
- }
- }
- });
- });
-
- Ok(promise)
-}
-
-// Module initialization
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
- cx.export_function("request", request)?;
- cx.export_function("getProfiles", get_profiles)?;
- cx.export_function("websocketConnect", websocket_connect)?;
- cx.export_function("websocketSend", websocket_send)?;
- cx.export_function("websocketClose", websocket_close)?;
- Ok(())
+ napi::register(&mut cx)
}
diff --git a/rust/src/napi/body.rs b/rust/src/napi/body.rs
new file mode 100644
index 0000000..db986ae
--- /dev/null
+++ b/rust/src/napi/body.rs
@@ -0,0 +1,63 @@
+use crate::store::body_store::{cancel_body, read_body_all, read_body_chunk};
+use neon::prelude::*;
+use neon::types::JsBuffer;
+
+fn read_body_chunk_js(mut cx: FunctionContext) -> JsResult {
+ let handle = cx.argument::(0)?.value(&mut cx) as u64;
+ let size = cx
+ .argument_opt(1)
+ .and_then(|value| value.downcast::(&mut cx).ok())
+ .map(|value| value.value(&mut cx) as usize)
+ .unwrap_or(65_536);
+
+ let channel = cx.channel();
+ let (deferred, promise) = cx.promise();
+
+ std::thread::spawn(move || {
+ let result = read_body_chunk(handle, size);
+
+ deferred.settle_with(&channel, move |mut cx| match result {
+ Ok((chunk, done)) => {
+ let obj = cx.empty_object();
+ let chunk_buffer = JsBuffer::from_slice(&mut cx, &chunk)?;
+ let done_value = cx.boolean(done);
+ obj.set(&mut cx, "chunk", chunk_buffer)?;
+ obj.set(&mut cx, "done", done_value)?;
+ Ok(obj)
+ }
+ Err(error) => cx.throw_error(format!("{:#}", error)),
+ });
+ });
+
+ Ok(promise)
+}
+
+fn read_body_all_js(mut cx: FunctionContext) -> JsResult {
+ let handle = cx.argument::(0)?.value(&mut cx) as u64;
+
+ let channel = cx.channel();
+ let (deferred, promise) = cx.promise();
+
+ std::thread::spawn(move || {
+ let result = read_body_all(handle);
+
+ deferred.settle_with(&channel, move |mut cx| match result {
+ Ok(bytes) => JsBuffer::from_slice(&mut cx, &bytes),
+ Err(error) => cx.throw_error(format!("{:#}", error)),
+ });
+ });
+
+ Ok(promise)
+}
+
+fn cancel_body_js(mut cx: FunctionContext) -> JsResult {
+ let handle = cx.argument::(0)?.value(&mut cx) as u64;
+ Ok(cx.boolean(cancel_body(handle)))
+}
+
+pub fn register(cx: &mut ModuleContext) -> NeonResult<()> {
+ cx.export_function("readBodyChunk", read_body_chunk_js)?;
+ cx.export_function("readBodyAll", read_body_all_js)?;
+ cx.export_function("cancelBody", cancel_body_js)?;
+ Ok(())
+}
diff --git a/rust/src/napi/convert.rs b/rust/src/napi/convert.rs
new file mode 100644
index 0000000..45fd8df
--- /dev/null
+++ b/rust/src/napi/convert.rs
@@ -0,0 +1,275 @@
+use crate::emulation::resolve_emulation;
+use crate::napi::profiles::parse_browser_emulation;
+use crate::transport::types::{
+ RequestOptions, Response, WebSocketConnectOptions, WebSocketConnection,
+};
+use neon::prelude::*;
+
+pub(crate) fn js_value_to_string_array(
+ cx: &mut FunctionContext,
+ value: Handle,
+) -> NeonResult> {
+ let array = value.downcast::(cx).or_throw(cx)?;
+ let mut strings = Vec::with_capacity(array.len(cx) as usize);
+
+ for item in array.to_vec(cx)? {
+ let string = item.downcast::(cx).or_throw(cx)?;
+ strings.push(string.value(cx));
+ }
+
+ Ok(strings)
+}
+
+pub(crate) fn js_value_to_header_tuples(
+ cx: &mut FunctionContext,
+ value: Handle,
+) -> NeonResult> {
+ let array = value.downcast::(cx).or_throw(cx)?;
+ let mut tuples = Vec::with_capacity(array.len(cx) as usize);
+
+ for item in array.to_vec(cx)? {
+ let tuple = item.downcast::(cx).or_throw(cx)?;
+ if tuple.len(cx) != 2 {
+ return cx.throw_type_error("Header tuple entries must contain exactly 2 items");
+ }
+
+ let name = tuple.get::(cx, 0)?.value(cx);
+ let value = tuple.get::(cx, 1)?.value(cx);
+ tuples.push((name, value));
+ }
+
+ Ok(tuples)
+}
+
+pub(crate) fn js_object_to_request_options(
+ cx: &mut FunctionContext,
+ obj: Handle,
+) -> NeonResult {
+ let url: Handle = obj.get(cx, "url")?;
+ let url = url.value(cx);
+
+ let browser_str = obj
+ .get_opt(cx, "browser")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx))
+ .unwrap_or_else(|| "chrome_137".to_string());
+
+ let emulation_json = obj
+ .get_opt(cx, "emulationJson")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx));
+
+ let emulation = resolve_emulation(
+ parse_browser_emulation(&browser_str),
+ emulation_json.as_deref(),
+ )
+ .or_else(|error| cx.throw_error(format!("{:#}", error)))?;
+
+ let method = obj
+ .get_opt(cx, "method")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx))
+ .unwrap_or_else(|| "GET".to_string());
+
+ let headers = obj
+ .get_opt(cx, "headers")?
+ .map(|v| js_value_to_header_tuples(cx, v))
+ .transpose()?
+ .unwrap_or_default();
+
+ let orig_headers = obj
+ .get_opt(cx, "origHeaders")?
+ .map(|v| js_value_to_string_array(cx, v))
+ .transpose()?
+ .unwrap_or_default();
+
+ let body = obj
+ .get_opt(cx, "body")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx));
+
+ let proxy = obj
+ .get_opt(cx, "proxy")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx));
+
+ let timeout = obj
+ .get_opt(cx, "timeout")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx) as u64)
+ .unwrap_or(30000);
+
+ let disable_default_headers = obj
+ .get_opt(cx, "disableDefaultHeaders")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx))
+ .unwrap_or(false);
+
+ let compress = obj
+ .get_opt(cx, "compress")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx))
+ .unwrap_or(true);
+
+ Ok(RequestOptions {
+ url,
+ emulation,
+ headers,
+ orig_headers,
+ method,
+ body,
+ proxy,
+ timeout,
+ disable_default_headers,
+ compress,
+ })
+}
+
+pub(crate) fn js_object_to_websocket_options(
+ cx: &mut FunctionContext,
+ obj: Handle,
+) -> NeonResult {
+ let url: Handle = obj.get(cx, "url")?;
+ let url = url.value(cx);
+
+ let browser_str = obj
+ .get_opt(cx, "browser")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx))
+ .unwrap_or_else(|| "chrome_137".to_string());
+
+ let emulation_json = obj
+ .get_opt(cx, "emulationJson")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx));
+
+ let emulation = resolve_emulation(
+ parse_browser_emulation(&browser_str),
+ emulation_json.as_deref(),
+ )
+ .or_else(|error| cx.throw_error(format!("{:#}", error)))?;
+
+ let headers = obj
+ .get_opt(cx, "headers")?
+ .map(|v| js_value_to_header_tuples(cx, v))
+ .transpose()?
+ .unwrap_or_default();
+
+ let orig_headers = obj
+ .get_opt(cx, "origHeaders")?
+ .map(|v| js_value_to_string_array(cx, v))
+ .transpose()?
+ .unwrap_or_default();
+
+ let proxy = obj
+ .get_opt(cx, "proxy")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx));
+
+ let timeout = obj
+ .get_opt(cx, "timeout")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx) as u64)
+ .unwrap_or(30000);
+
+ let disable_default_headers = obj
+ .get_opt(cx, "disableDefaultHeaders")?
+ .and_then(|v: Handle| v.downcast::(cx).ok())
+ .map(|v| v.value(cx))
+ .unwrap_or(false);
+
+ let mut protocols = Vec::new();
+ if let Some(values) = obj.get_opt::(cx, "protocols")? {
+ for value in values.to_vec(cx)? {
+ if let Ok(value) = value.downcast::(cx) {
+ protocols.push(value.value(cx));
+ }
+ }
+ }
+
+ Ok(WebSocketConnectOptions {
+ url,
+ emulation,
+ headers,
+ orig_headers,
+ proxy,
+ timeout,
+ disable_default_headers,
+ protocols,
+ })
+}
+
+pub(crate) fn response_to_js_object<'a, C: Context<'a>>(
+ cx: &mut C,
+ response: Response,
+) -> JsResult<'a, JsObject> {
+ let obj = cx.empty_object();
+
+ let status = cx.number(response.status as f64);
+ obj.set(cx, "status", status)?;
+
+ let url = cx.string(&response.url);
+ obj.set(cx, "url", url)?;
+
+ let headers_obj = cx.empty_object();
+ for (key, value) in response.headers {
+ let value_str = cx.string(&value);
+ headers_obj.set(cx, key.as_str(), value_str)?;
+ }
+ obj.set(cx, "headers", headers_obj)?;
+
+ let cookies_obj = cx.empty_object();
+ for (key, value) in response.cookies {
+ let value_str = cx.string(&value);
+ cookies_obj.set(cx, key.as_str(), value_str)?;
+ }
+ obj.set(cx, "cookies", cookies_obj)?;
+
+ let set_cookies = JsArray::new(cx, response.set_cookies.len());
+ for (index, value) in response.set_cookies.into_iter().enumerate() {
+ let value_str = cx.string(&value);
+ set_cookies.set(cx, index as u32, value_str)?;
+ }
+ obj.set(cx, "setCookies", set_cookies)?;
+
+ let body_handle = cx.number(response.body_handle as f64);
+ obj.set(cx, "bodyHandle", body_handle)?;
+
+ Ok(obj)
+}
+
+pub(crate) fn websocket_to_js_object<'a, C: Context<'a>>(
+ cx: &mut C,
+ websocket: WebSocketConnection,
+) -> JsResult<'a, JsObject> {
+ let obj = cx.empty_object();
+ let handle = cx.number(websocket.handle as f64);
+ let url = cx.string(&websocket.url);
+
+ obj.set(cx, "handle", handle)?;
+ obj.set(cx, "url", url)?;
+
+ match websocket.protocol {
+ Some(protocol) => {
+ let value = cx.string(protocol);
+ obj.set(cx, "protocol", value)?;
+ }
+ None => {
+ let value = cx.null();
+ obj.set(cx, "protocol", value)?;
+ }
+ };
+
+ match websocket.extensions {
+ Some(extensions) => {
+ let value = cx.string(extensions);
+ obj.set(cx, "extensions", value)?;
+ }
+ None => {
+ let value = cx.null();
+ obj.set(cx, "extensions", value)?;
+ }
+ };
+
+ Ok(obj)
+}
diff --git a/rust/src/napi/mod.rs b/rust/src/napi/mod.rs
new file mode 100644
index 0000000..db48e6e
--- /dev/null
+++ b/rust/src/napi/mod.rs
@@ -0,0 +1,15 @@
+mod body;
+mod convert;
+mod profiles;
+mod request;
+mod websocket;
+
+use neon::prelude::*;
+
+pub fn register(cx: &mut ModuleContext) -> NeonResult<()> {
+ request::register(cx)?;
+ body::register(cx)?;
+ websocket::register(cx)?;
+ profiles::register(cx)?;
+ Ok(())
+}
diff --git a/rust/src/napi/profiles.rs b/rust/src/napi/profiles.rs
new file mode 100644
index 0000000..751680e
--- /dev/null
+++ b/rust/src/napi/profiles.rs
@@ -0,0 +1,56 @@
+use neon::prelude::*;
+use std::collections::HashMap;
+use std::sync::OnceLock;
+use strum::VariantArray;
+use wreq_util::Emulation as BrowserEmulation;
+
+static PROFILE_NAMES: OnceLock> = OnceLock::new();
+static PROFILE_MAP: OnceLock> = OnceLock::new();
+
+fn serialize_emulation_name(emulation: BrowserEmulation) -> String {
+ serde_json::to_string(&emulation)
+ .expect("failed to serialize emulation profile")
+ .trim_matches('"')
+ .replace('.', "_")
+}
+
+fn profile_names() -> &'static Vec {
+ PROFILE_NAMES.get_or_init(|| {
+ BrowserEmulation::VARIANTS
+ .iter()
+ .map(|emulation| serialize_emulation_name(*emulation))
+ .collect()
+ })
+}
+
+fn profile_map() -> &'static HashMap {
+ PROFILE_MAP.get_or_init(|| {
+ BrowserEmulation::VARIANTS
+ .iter()
+ .map(|emulation| (serialize_emulation_name(*emulation), *emulation))
+ .collect()
+ })
+}
+
+pub(crate) fn parse_browser_emulation(browser: &str) -> BrowserEmulation {
+ profile_map()
+ .get(browser)
+ .copied()
+ .unwrap_or(BrowserEmulation::Chrome137)
+}
+
+fn get_profiles(mut cx: FunctionContext) -> JsResult {
+ let js_array = cx.empty_array();
+
+ for (i, profile) in profile_names().iter().enumerate() {
+ let js_string = cx.string(profile);
+ js_array.set(&mut cx, i as u32, js_string)?;
+ }
+
+ Ok(js_array)
+}
+
+pub fn register(cx: &mut ModuleContext) -> NeonResult<()> {
+ cx.export_function("getProfiles", get_profiles)?;
+ Ok(())
+}
diff --git a/rust/src/napi/request.rs b/rust/src/napi/request.rs
new file mode 100644
index 0000000..1447b2a
--- /dev/null
+++ b/rust/src/napi/request.rs
@@ -0,0 +1,27 @@
+use crate::napi::convert::{js_object_to_request_options, response_to_js_object};
+use crate::transport::execute_request;
+use neon::prelude::*;
+
+fn request(mut cx: FunctionContext) -> JsResult {
+ let options_obj = cx.argument::(0)?;
+ let options = js_object_to_request_options(&mut cx, options_obj)?;
+
+ let channel = cx.channel();
+ let (deferred, promise) = cx.promise();
+
+ std::thread::spawn(move || {
+ let result = execute_request(options);
+
+ deferred.settle_with(&channel, move |mut cx| match result {
+ Ok(response) => response_to_js_object(&mut cx, response),
+ Err(error) => cx.throw_error(format!("{:#}", error)),
+ });
+ });
+
+ Ok(promise)
+}
+
+pub fn register(cx: &mut ModuleContext) -> NeonResult<()> {
+ cx.export_function("request", request)?;
+ Ok(())
+}
diff --git a/rust/src/napi/websocket.rs b/rust/src/napi/websocket.rs
new file mode 100644
index 0000000..bf8b4ef
--- /dev/null
+++ b/rust/src/napi/websocket.rs
@@ -0,0 +1,150 @@
+use crate::napi::convert::{js_object_to_websocket_options, websocket_to_js_object};
+use crate::store::websocket_store::{
+ close_websocket, read_websocket_message, send_websocket_binary, send_websocket_text,
+};
+use crate::transport::{connect_websocket, types::WebSocketReadResult};
+use neon::prelude::*;
+use neon::types::buffer::TypedArray;
+use neon::types::JsBuffer;
+
+fn websocket_connect_js(mut cx: FunctionContext) -> JsResult {
+ let options_obj = cx.argument::(0)?;
+ let options = js_object_to_websocket_options(&mut cx, options_obj)?;
+
+ let channel = cx.channel();
+ let (deferred, promise) = cx.promise();
+
+ std::thread::spawn(move || {
+ let result = connect_websocket(options);
+
+ deferred.settle_with(&channel, move |mut cx| match result {
+ Ok(websocket) => websocket_to_js_object(&mut cx, websocket),
+ Err(error) => cx.throw_error(format!("{:#}", error)),
+ });
+ });
+
+ Ok(promise)
+}
+
+fn websocket_read_js(mut cx: FunctionContext) -> JsResult {
+ let handle = cx.argument::(0)?.value(&mut cx) as u64;
+
+ let channel = cx.channel();
+ let (deferred, promise) = cx.promise();
+
+ std::thread::spawn(move || {
+ let result = read_websocket_message(handle);
+
+ deferred.settle_with(&channel, move |mut cx| match result {
+ Ok(WebSocketReadResult::Text(text)) => {
+ let obj = cx.empty_object();
+ let type_value = cx.string("text");
+ let data_value = cx.string(text);
+ obj.set(&mut cx, "type", type_value)?;
+ obj.set(&mut cx, "data", data_value)?;
+ Ok(obj)
+ }
+ Ok(WebSocketReadResult::Binary(bytes)) => {
+ let obj = cx.empty_object();
+ let type_value = cx.string("binary");
+ let data_value = JsBuffer::from_slice(&mut cx, &bytes)?;
+ obj.set(&mut cx, "type", type_value)?;
+ obj.set(&mut cx, "data", data_value)?;
+ Ok(obj)
+ }
+ Ok(WebSocketReadResult::Close {
+ code,
+ reason,
+ was_clean,
+ }) => {
+ let obj = cx.empty_object();
+ let type_value = cx.string("close");
+ let code_value = cx.number(code as f64);
+ let reason_value = cx.string(reason);
+ let was_clean_value = cx.boolean(was_clean);
+ obj.set(&mut cx, "type", type_value)?;
+ obj.set(&mut cx, "code", code_value)?;
+ obj.set(&mut cx, "reason", reason_value)?;
+ obj.set(&mut cx, "wasClean", was_clean_value)?;
+ Ok(obj)
+ }
+ Err(error) => cx.throw_error(format!("{:#}", error)),
+ });
+ });
+
+ Ok(promise)
+}
+
+fn websocket_send_text_js(mut cx: FunctionContext) -> JsResult {
+ let handle = cx.argument::(0)?.value(&mut cx) as u64;
+ let text = cx.argument::(1)?.value(&mut cx);
+
+ let channel = cx.channel();
+ let (deferred, promise) = cx.promise();
+
+ std::thread::spawn(move || {
+ let result = send_websocket_text(handle, text);
+
+ deferred.settle_with(&channel, move |mut cx| match result {
+ Ok(()) => Ok(cx.undefined()),
+ Err(error) => cx.throw_error(format!("{:#}", error)),
+ });
+ });
+
+ Ok(promise)
+}
+
+fn websocket_send_binary_js(mut cx: FunctionContext) -> JsResult {
+ let handle = cx.argument::(0)?.value(&mut cx) as u64;
+ let buffer = cx.argument::(1)?;
+ let bytes = buffer.as_slice(&cx).to_vec();
+
+ let channel = cx.channel();
+ let (deferred, promise) = cx.promise();
+
+ std::thread::spawn(move || {
+ let result = send_websocket_binary(handle, bytes);
+
+ deferred.settle_with(&channel, move |mut cx| match result {
+ Ok(()) => Ok(cx.undefined()),
+ Err(error) => cx.throw_error(format!("{:#}", error)),
+ });
+ });
+
+ Ok(promise)
+}
+
+fn websocket_close_js(mut cx: FunctionContext) -> JsResult {
+ let handle = cx.argument::(0)?.value(&mut cx) as u64;
+ let code = cx
+ .argument_opt(1)
+ .and_then(|value| value.downcast::(&mut cx).ok())
+ .map(|value| value.value(&mut cx) as u16);
+ let reason = cx
+ .argument_opt(2)
+ .and_then(|value| value.downcast::(&mut cx).ok())
+ .map(|value| value.value(&mut cx));
+
+ let channel = cx.channel();
+ let (deferred, promise) = cx.promise();
+
+ std::thread::spawn(move || {
+ let result = close_websocket(handle, code, reason);
+
+ deferred.settle_with(&channel, move |mut cx| match result {
+ Ok(()) => Ok(cx.undefined()),
+ Err(error) => cx.throw_error(format!("{:#}", error)),
+ });
+ });
+
+ Ok(promise)
+}
+
+pub fn register(cx: &mut ModuleContext) -> NeonResult<()> {
+ cx.export_function("websocketConnect", websocket_connect_js)?;
+ cx.export_function("websocketRead", websocket_read_js)?;
+ cx.export_function("websocketSendText", websocket_send_text_js)?;
+ cx.export_function("websocketSendBinary", websocket_send_binary_js)?;
+ cx.export_function("websocketClose", websocket_close_js)?;
+ Ok(())
+}
diff --git a/rust/src/store/body_store.rs b/rust/src/store/body_store.rs
new file mode 100644
index 0000000..e00e62e
--- /dev/null
+++ b/rust/src/store/body_store.rs
@@ -0,0 +1,82 @@
+use crate::store::runtime::runtime;
+use anyhow::{Context, Result};
+use std::collections::HashMap;
+use std::sync::{
+ atomic::{AtomicU64, Ordering},
+ Mutex, OnceLock,
+};
+
+#[derive(Debug)]
+struct StoredBody {
+ response: wreq::Response,
+}
+
+static NEXT_BODY_HANDLE: AtomicU64 = AtomicU64::new(1);
+static BODY_STORE: OnceLock>> = OnceLock::new();
+
+fn body_store() -> &'static Mutex> {
+ BODY_STORE.get_or_init(|| Mutex::new(HashMap::new()))
+}
+
+pub fn store_body(response: wreq::Response) -> u64 {
+ let handle = NEXT_BODY_HANDLE.fetch_add(1, Ordering::Relaxed);
+ body_store()
+ .lock()
+ .expect("body store poisoned")
+ .insert(handle, StoredBody { response });
+ handle
+}
+
+pub fn read_body_chunk(handle: u64, _size: usize) -> Result<(Vec, bool)> {
+ let mut store = body_store()
+ .lock()
+ .map_err(|_| anyhow::anyhow!("body store poisoned"))?;
+ let Some(body) = store.get_mut(&handle) else {
+ return Err(anyhow::anyhow!("Unknown body handle: {}", handle));
+ };
+
+ let chunk = runtime()
+ .block_on(body.response.chunk())
+ .context("Failed to read response body chunk")?;
+
+ let Some(chunk) = chunk else {
+ store.remove(&handle);
+ return Ok((Vec::new(), true));
+ };
+
+ Ok((chunk.to_vec(), false))
+}
+
+pub fn read_body_all(handle: u64) -> Result> {
+ let mut store = body_store()
+ .lock()
+ .map_err(|_| anyhow::anyhow!("body store poisoned"))?;
+ let Some(body) = store.remove(&handle) else {
+ return Err(anyhow::anyhow!("Unknown body handle: {}", handle));
+ };
+
+ let mut bytes = Vec::new();
+ let mut response = body.response;
+
+ runtime().block_on(async {
+ while let Some(chunk) = response
+ .chunk()
+ .await
+ .context("Failed to read response body chunk")?
+ {
+ bytes.extend_from_slice(&chunk);
+ }
+
+ Ok::<(), anyhow::Error>(())
+ })?;
+
+ Ok(bytes)
+}
+
+pub fn cancel_body(handle: u64) -> bool {
+ body_store()
+ .lock()
+ .expect("body store poisoned")
+ .remove(&handle)
+ .is_some()
+}
diff --git a/rust/src/store/mod.rs b/rust/src/store/mod.rs
new file mode 100644
index 0000000..b27735a
--- /dev/null
+++ b/rust/src/store/mod.rs
@@ -0,0 +1,3 @@
+pub mod body_store;
+pub mod runtime;
+pub mod websocket_store;
diff --git a/rust/src/store/runtime.rs b/rust/src/store/runtime.rs
new file mode 100644
index 0000000..990a0ba
--- /dev/null
+++ b/rust/src/store/runtime.rs
@@ -0,0 +1,8 @@
+use std::sync::OnceLock;
+
+static TOKIO_RUNTIME: OnceLock = OnceLock::new();
+
+pub fn runtime() -> &'static tokio::runtime::Runtime {
+ TOKIO_RUNTIME
+ .get_or_init(|| tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"))
+}
diff --git a/rust/src/store/websocket_store.rs b/rust/src/store/websocket_store.rs
new file mode 100644
index 0000000..6458502
--- /dev/null
+++ b/rust/src/store/websocket_store.rs
@@ -0,0 +1,112 @@
+use crate::transport::types::WebSocketReadResult;
+use anyhow::Result;
+use std::collections::HashMap;
+use std::sync::{
+ atomic::{AtomicU64, Ordering},
+ Arc, Mutex, OnceLock,
+};
+
+#[derive(Debug)]
+pub(crate) enum WebSocketCommand {
+ Text(String),
+ Binary(Vec),
+ Close {
+ code: Option,
+ reason: Option,
+ },
+}
+
+#[derive(Debug)]
+pub(crate) struct StoredWebSocket {
+ pub commands: tokio::sync::mpsc::UnboundedSender,
+ pub events: tokio::sync::Mutex>,
+}
+
+pub(crate) type SharedWebSocket = Arc;
+
+static NEXT_WEBSOCKET_HANDLE: AtomicU64 = AtomicU64::new(1);
+static WEBSOCKET_STORE: OnceLock>> = OnceLock::new();
+
+fn websocket_store() -> &'static Mutex> {
+ WEBSOCKET_STORE.get_or_init(|| Mutex::new(HashMap::new()))
+}
+
+pub(crate) fn insert_websocket(
+ commands: tokio::sync::mpsc::UnboundedSender,
+ events: tokio::sync::mpsc::UnboundedReceiver,
+) -> u64 {
+ let handle = NEXT_WEBSOCKET_HANDLE.fetch_add(1, Ordering::Relaxed);
+
+ websocket_store()
+ .lock()
+ .expect("websocket store poisoned")
+ .insert(
+ handle,
+ Arc::new(StoredWebSocket {
+ commands,
+ events: tokio::sync::Mutex::new(events),
+ }),
+ );
+
+ handle
+}
+
+fn get_websocket(handle: u64) -> Result {
+ let store = websocket_store()
+ .lock()
+ .map_err(|_| anyhow::anyhow!("websocket store poisoned"))?;
+
+ store
+ .get(&handle)
+ .cloned()
+ .ok_or_else(|| anyhow::anyhow!("Unknown websocket handle: {}", handle))
+}
+
+pub(crate) fn remove_websocket(handle: u64) {
+ websocket_store()
+ .lock()
+ .expect("websocket store poisoned")
+ .remove(&handle);
+}
+
+pub fn read_websocket_message(handle: u64) -> Result {
+ let websocket = get_websocket(handle)?;
+
+ let result = crate::store::runtime::runtime().block_on(async {
+ let mut events = websocket.events.lock().await;
+ events
+ .recv()
+ .await
+ .ok_or_else(|| anyhow::anyhow!("WebSocket event stream is closed"))
+ });
+
+ if matches!(result, Ok(WebSocketReadResult::Close { .. })) {
+ remove_websocket(handle);
+ }
+
+ result
+}
+
+pub fn send_websocket_text(handle: u64, text: String) -> Result<()> {
+ let websocket = get_websocket(handle)?;
+ websocket
+ .commands
+ .send(WebSocketCommand::Text(text))
+ .map_err(|_| anyhow::anyhow!("WebSocket is already closed"))
+}
+
+pub fn send_websocket_binary(handle: u64, bytes: Vec) -> Result<()> {
+ let websocket = get_websocket(handle)?;
+ websocket
+ .commands
+ .send(WebSocketCommand::Binary(bytes))
+ .map_err(|_| anyhow::anyhow!("WebSocket is already closed"))
+}
+
+pub fn close_websocket(handle: u64, code: Option, reason: Option) -> Result<()> {
+ let websocket = get_websocket(handle)?;
+ websocket
+ .commands
+ .send(WebSocketCommand::Close { code, reason })
+ .map_err(|_| anyhow::anyhow!("WebSocket is already closed"))
+}
diff --git a/rust/src/transport/cookies.rs b/rust/src/transport/cookies.rs
new file mode 100644
index 0000000..17f6d55
--- /dev/null
+++ b/rust/src/transport/cookies.rs
@@ -0,0 +1,6 @@
+pub fn parse_cookie_pair(set_cookie: &str) -> Option<(String, String)> {
+ let pair = set_cookie.split(';').next()?.trim();
+ let (name, value) = pair.split_once('=')?;
+
+ Some((name.to_string(), value.to_string()))
+}
diff --git a/rust/src/transport/headers.rs b/rust/src/transport/headers.rs
new file mode 100644
index 0000000..f31b265
--- /dev/null
+++ b/rust/src/transport/headers.rs
@@ -0,0 +1,9 @@
+use wreq::header::OrigHeaderMap;
+
+pub fn build_orig_header_map(orig_headers: &[String]) -> OrigHeaderMap {
+ let mut map = OrigHeaderMap::with_capacity(orig_headers.len());
+ for header in orig_headers {
+ map.insert(header.clone());
+ }
+ map
+}
diff --git a/rust/src/transport/mod.rs b/rust/src/transport/mod.rs
new file mode 100644
index 0000000..b41b8f6
--- /dev/null
+++ b/rust/src/transport/mod.rs
@@ -0,0 +1,8 @@
+mod cookies;
+mod headers;
+mod request;
+pub mod types;
+mod websocket;
+
+pub use request::execute_request;
+pub use websocket::connect_websocket;
diff --git a/rust/src/client.rs b/rust/src/transport/request.rs
similarity index 58%
rename from rust/src/client.rs
rename to rust/src/transport/request.rs
index b31752e..df27cb0 100644
--- a/rust/src/client.rs
+++ b/rust/src/transport/request.rs
@@ -1,42 +1,28 @@
+use crate::store::body_store::store_body;
+use crate::store::runtime::runtime;
+use crate::transport::cookies::parse_cookie_pair;
+use crate::transport::headers::build_orig_header_map;
+use crate::transport::types::{RequestOptions, Response};
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::time::Duration;
-use wreq_util::Emulation;
-
-#[derive(Debug, Clone)]
-pub struct RequestOptions {
- pub url: String,
- pub emulation: Emulation,
- pub headers: HashMap,
- pub method: String,
- pub body: Option,
- pub proxy: Option,
- pub timeout: u64,
-}
+use wreq::redirect;
-#[derive(Debug, Clone)]
-pub struct Response {
- pub status: u16,
- pub headers: HashMap,
- pub body: String,
- pub cookies: HashMap,
- pub url: String,
+pub fn execute_request(options: RequestOptions) -> Result {
+ runtime().block_on(make_request(options))
}
pub async fn make_request(options: RequestOptions) -> Result {
- // Create client builder with emulation
let mut client_builder = wreq::Client::builder()
.emulation(options.emulation)
.cookie_store(true);
- // Apply proxy if present (must be set at client builder level)
if let Some(proxy_url) = &options.proxy {
- let proxy = wreq::Proxy::all(proxy_url)
- .context("Failed to create proxy")?;
+ let proxy = wreq::Proxy::all(proxy_url).context("Failed to create proxy")?;
client_builder = client_builder.proxy(proxy);
}
- // Build the client
+ let orig_headers = build_orig_header_map(&options.orig_headers);
let client = client_builder
.build()
.context("Failed to build HTTP client")?;
@@ -47,7 +33,6 @@ pub async fn make_request(options: RequestOptions) -> Result {
&options.method
};
- // Build request
let mut request = match method.to_uppercase().as_str() {
"GET" => client.get(&options.url),
"POST" => client.post(&options.url),
@@ -58,30 +43,33 @@ pub async fn make_request(options: RequestOptions) -> Result {
_ => return Err(anyhow::anyhow!("Unsupported HTTP method: {}", method)),
};
- // Apply custom headers
for (key, value) in &options.headers {
request = request.header(key, value);
}
- // Apply body if present
+ if !orig_headers.is_empty() {
+ request = request.orig_headers(orig_headers);
+ }
+
if let Some(body) = options.body {
request = request.body(body);
}
- // Apply timeout
request = request.timeout(Duration::from_millis(options.timeout));
+ request = request.redirect(redirect::Policy::none());
+ request = request.default_headers(!options.disable_default_headers);
+ request = request.gzip(options.compress);
+ request = request.brotli(options.compress);
+ request = request.deflate(options.compress);
- // Execute request
let response = request
.send()
.await
.with_context(|| format!("{} {}", method, options.url))?;
- // Extract response data
let status = response.status().as_u16();
let final_url = response.uri().to_string();
- // Extract headers
let mut response_headers = HashMap::new();
for (key, value) in response.headers() {
if let Ok(value_str) = value.to_str() {
@@ -89,30 +77,26 @@ pub async fn make_request(options: RequestOptions) -> Result {
}
}
- // Extract cookies
let mut cookies = HashMap::new();
- if let Some(cookie_header) = response.headers().get("set-cookie") {
+ let mut set_cookies = Vec::new();
+ for cookie_header in response.headers().get_all("set-cookie") {
if let Ok(cookie_str) = cookie_header.to_str() {
- // Simple cookie parsing (name=value)
- for cookie_part in cookie_str.split(';') {
- if let Some((key, value)) = cookie_part.trim().split_once('=') {
- cookies.insert(key.to_string(), value.to_string());
- }
+ set_cookies.push(cookie_str.to_string());
+
+ if let Some((key, value)) = parse_cookie_pair(cookie_str) {
+ cookies.insert(key, value);
}
}
}
- // Get body
- let body = response
- .text()
- .await
- .context("Failed to read response body")?;
+ let body_handle = store_body(response);
Ok(Response {
status,
headers: response_headers,
- body,
+ body_handle,
cookies,
+ set_cookies,
url: final_url,
})
}
diff --git a/rust/src/transport/types.rs b/rust/src/transport/types.rs
new file mode 100644
index 0000000..f4c036d
--- /dev/null
+++ b/rust/src/transport/types.rs
@@ -0,0 +1,57 @@
+use std::collections::HashMap;
+use wreq::Emulation;
+
+#[derive(Debug, Clone)]
+pub struct RequestOptions {
+ pub url: String,
+ pub emulation: Emulation,
+ pub headers: Vec<(String, String)>,
+ pub orig_headers: Vec,
+ pub method: String,
+ pub body: Option,
+ pub proxy: Option,
+ pub timeout: u64,
+ pub disable_default_headers: bool,
+ pub compress: bool,
+}
+
+#[derive(Debug, Clone)]
+pub struct Response {
+ pub status: u16,
+ pub headers: HashMap,
+ pub body_handle: u64,
+ pub cookies: HashMap,
+ pub set_cookies: Vec,
+ pub url: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct WebSocketConnectOptions {
+ pub url: String,
+ pub emulation: Emulation,
+ pub headers: Vec<(String, String)>,
+ pub orig_headers: Vec,
+ pub proxy: Option,
+ pub timeout: u64,
+ pub disable_default_headers: bool,
+ pub protocols: Vec,
+}
+
+#[derive(Debug, Clone)]
+pub struct WebSocketConnection {
+ pub handle: u64,
+ pub protocol: Option,
+ pub extensions: Option,
+ pub url: String,
+}
+
+#[derive(Debug, Clone)]
+pub enum WebSocketReadResult {
+ Text(String),
+ Binary(Vec),
+ Close {
+ code: u16,
+ reason: String,
+ was_clean: bool,
+ },
+}
diff --git a/rust/src/transport/websocket.rs b/rust/src/transport/websocket.rs
new file mode 100644
index 0000000..b93b120
--- /dev/null
+++ b/rust/src/transport/websocket.rs
@@ -0,0 +1,196 @@
+use crate::store::runtime::runtime;
+use crate::store::websocket_store::{insert_websocket, WebSocketCommand};
+use crate::transport::headers::build_orig_header_map;
+use crate::transport::types::{WebSocketConnectOptions, WebSocketConnection, WebSocketReadResult};
+use anyhow::{Context, Result};
+use std::time::Duration;
+use wreq::ws::message::{CloseCode, CloseFrame, Message};
+use wreq::ws::WebSocket;
+
+pub fn connect_websocket(options: WebSocketConnectOptions) -> Result {
+ runtime().block_on(make_websocket(options))
+}
+
+async fn run_websocket_task(
+ mut websocket: WebSocket,
+ mut commands: tokio::sync::mpsc::UnboundedReceiver,
+ events: tokio::sync::mpsc::UnboundedSender,
+) {
+ let mut close_requested = false;
+ let mut requested_close_code = 1000;
+ let mut requested_close_reason = String::new();
+
+ loop {
+ tokio::select! {
+ command = commands.recv() => {
+ match command {
+ Some(WebSocketCommand::Text(text)) => {
+ if websocket.send(Message::Text(text.into())).await.is_err() {
+ let _ = events.send(WebSocketReadResult::Close {
+ code: 1006,
+ reason: String::new(),
+ was_clean: false,
+ });
+ break;
+ }
+ }
+ Some(WebSocketCommand::Binary(bytes)) => {
+ if websocket.send(Message::Binary(bytes.into())).await.is_err() {
+ let _ = events.send(WebSocketReadResult::Close {
+ code: 1006,
+ reason: String::new(),
+ was_clean: false,
+ });
+ break;
+ }
+ }
+ Some(WebSocketCommand::Close { code, reason }) => {
+ close_requested = true;
+ requested_close_code = code.unwrap_or(1000);
+ requested_close_reason = reason.unwrap_or_default();
+
+ let frame = Message::Close(Some(CloseFrame {
+ code: CloseCode::from(requested_close_code),
+ reason: requested_close_reason.clone().into(),
+ }));
+
+ if websocket.send(frame).await.is_err() {
+ let _ = events.send(WebSocketReadResult::Close {
+ code: 1006,
+ reason: String::new(),
+ was_clean: false,
+ });
+ break;
+ }
+ }
+ None => {
+ break;
+ }
+ }
+ }
+ message = websocket.recv() => {
+ match message {
+ Some(Ok(Message::Text(text))) => {
+ if events.send(WebSocketReadResult::Text(text.to_string())).is_err() {
+ break;
+ }
+ }
+ Some(Ok(Message::Binary(bytes))) => {
+ if events.send(WebSocketReadResult::Binary(bytes.to_vec())).is_err() {
+ break;
+ }
+ }
+ Some(Ok(Message::Close(frame))) => {
+ let (code, reason) = match frame {
+ Some(frame) => (u16::from(frame.code), frame.reason.to_string()),
+ None => {
+ if close_requested {
+ (requested_close_code, requested_close_reason.clone())
+ } else {
+ (1005, String::new())
+ }
+ }
+ };
+
+ let _ = events.send(WebSocketReadResult::Close {
+ code,
+ reason,
+ was_clean: true,
+ });
+ break;
+ }
+ Some(Ok(Message::Ping(_))) | Some(Ok(Message::Pong(_))) => {}
+ Some(Err(_)) => {
+ let _ = events.send(WebSocketReadResult::Close {
+ code: 1006,
+ reason: String::new(),
+ was_clean: false,
+ });
+ break;
+ }
+ None => {
+ let _ = events.send(WebSocketReadResult::Close {
+ code: if close_requested {
+ requested_close_code
+ } else {
+ 1006
+ },
+ reason: if close_requested {
+ requested_close_reason.clone()
+ } else {
+ String::new()
+ },
+ was_clean: close_requested,
+ });
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+async fn make_websocket(options: WebSocketConnectOptions) -> Result {
+ let mut client_builder = wreq::Client::builder()
+ .emulation(options.emulation)
+ .cookie_store(true)
+ .timeout(Duration::from_millis(options.timeout));
+
+ if let Some(proxy_url) = &options.proxy {
+ let proxy = wreq::Proxy::all(proxy_url).context("Failed to create proxy")?;
+ client_builder = client_builder.proxy(proxy);
+ }
+
+ let client = client_builder
+ .build()
+ .context("Failed to build WebSocket client")?;
+
+ let mut request = client.websocket(&options.url);
+ let orig_headers = build_orig_header_map(&options.orig_headers);
+ for (key, value) in &options.headers {
+ request = request.header(key, value);
+ }
+
+ if !orig_headers.is_empty() {
+ request = request.orig_headers(orig_headers);
+ }
+
+ request = request.default_headers(!options.disable_default_headers);
+
+ if !options.protocols.is_empty() {
+ request = request.protocols(options.protocols.iter().cloned());
+ }
+
+ let response = request
+ .send()
+ .await
+ .with_context(|| format!("WS {}", options.url))?;
+
+ let extensions = response
+ .headers()
+ .get("sec-websocket-extensions")
+ .and_then(|value| value.to_str().ok())
+ .map(str::to_owned);
+
+ let websocket = response
+ .into_websocket()
+ .await
+ .with_context(|| format!("WS upgrade {}", options.url))?;
+
+ let protocol = websocket
+ .protocol()
+ .and_then(|value| value.to_str().ok())
+ .map(str::to_owned);
+
+ let (command_tx, command_rx) = tokio::sync::mpsc::unbounded_channel();
+ let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
+ runtime().spawn(run_websocket_task(websocket, command_rx, event_tx));
+ let handle = insert_websocket(command_tx, event_rx);
+
+ Ok(WebSocketConnection {
+ handle,
+ protocol,
+ extensions,
+ url: options.url,
+ })
+}
diff --git a/rust/src/websocket.rs b/rust/src/websocket.rs
deleted file mode 100644
index 12bce25..0000000
--- a/rust/src/websocket.rs
+++ /dev/null
@@ -1,146 +0,0 @@
-use anyhow::{Context, Result};
-use futures_util::{SinkExt, StreamExt};
-use neon::prelude::*;
-use std::collections::HashMap;
-use std::sync::{Arc, Mutex as StdMutex};
-use tokio::sync::Mutex;
-use wreq::ws::message::Message;
-use wreq::ws::WebSocket;
-use wreq_util::Emulation;
-use once_cell::sync::Lazy;
-
-// Global storage for WebSocket connections
-static WS_CONNECTIONS: Lazy>>> =
- Lazy::new(|| StdMutex::new(HashMap::new()));
-
-static NEXT_WS_ID: Lazy> = Lazy::new(|| StdMutex::new(1));
-
-// Global Tokio runtime for WebSocket operations
-pub static WS_RUNTIME: Lazy