diff --git a/.github/assets/logo/dark.svg b/.github/assets/logo/dark.svg
new file mode 100644
index 0000000..b2541ff
--- /dev/null
+++ b/.github/assets/logo/dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/.github/assets/logo/light.svg b/.github/assets/logo/light.svg
new file mode 100644
index 0000000..4555cf0
--- /dev/null
+++ b/.github/assets/logo/light.svg
@@ -0,0 +1,3 @@
+
diff --git a/.gitignore b/.gitignore
index 1ed911d..12fb8f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
.idea
node_modules
dist
+coverage
diff --git a/README.md b/README.md
index b2567ce..d0a1925 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,13 @@
-# ZenRows Node.js SDK
+
+
+
+
+
+
-SDK to access [ZenRows](https://www.zenrows.com/) API directly from Node.js. ZenRows handles proxies rotation, headless browsers, and CAPTCHAs for you.
+# Zenrows Node.js SDK
+
+SDK to access [Zenrows](https://www.zenrows.com/) API directly from Node.js. Zenrows handles proxies rotation, headless browsers, and CAPTCHAs for you.
## Installation
@@ -21,6 +28,8 @@ The SDK uses the official [fetch api](https://nodejs.org/dist/latest-v18.x/docs/
It also uses [fetch-retry](https://github.com/jonbern/fetch-retry) to automatically retry failed requests (status code 429 and 5XX). Retries are not active by default; you need to specify the number of retries, as shown below. It already includes an exponential back-off retry delay between failed requests.
+> `client.fetch()` is the primary method for the main page-scraping product. `client.get()` still works and is kept as a deprecated alias — new code should call `fetch()`.
+
```javascript
const { ZenRows } = require("zenrows");
@@ -30,7 +39,7 @@ const url = "https://www.zenrows.com/";
(async () => {
const client = new ZenRows(apiKey, { retries: 1 });
- const response = await client.get(
+ const response = await client.fetch(
url,
{
// Our algorithm allows to automatically extract content from any website
@@ -136,6 +145,70 @@ const url = "https://httpbin.org/anything";
})();
```
+### Extract
+
+[Extract](https://docs.zenrows.com) (beta) runs a page through Zenrows' AI-powered structured extraction instead of returning raw HTML. Use `client.extract()` — it's the same request as `fetch()`, with the `extract` param set for you (defaults to `"auto"`; pass `"native"` or `"standard"` for the other contracts).
+
+```javascript
+const { ZenRows } = require("zenrows");
+
+const apiKey = "YOUR-API-KEY";
+const url = "https://www.zenrows.com/";
+
+(async () => {
+ const client = new ZenRows(apiKey);
+
+ const response = await client.extract(url); // extract: "auto"
+ // const response = await client.extract(url, { extract: "native" });
+
+ const data = await response.json();
+ console.log(data);
+})();
+```
+
+### Batch
+
+The [Batch API](https://docs.zenrows.com) (beta) runs many URLs asynchronously as a job, and is reachable via `client.batch`. A job is either **open** — it keeps accepting tasks via `addTasks()` until you `closeJob()` it, useful when you're streaming URLs in over time — or a normal one-shot batch where every task is known upfront.
+
+```javascript
+const { ZenRows } = require("zenrows");
+
+const apiKey = "YOUR-API-KEY";
+
+(async () => {
+ const client = new ZenRows(apiKey);
+
+ // One-shot batch: every task known upfront.
+ const job = await client.batch.submitJob({
+ tasks: [{ url: "https://example.com/1" }, { url: "https://example.com/2" }],
+ });
+
+ // Streaming batch: keep the job open, add tasks as they arrive, close when done.
+ const streamingJob = await client.batch.submitJob({ status: "open" });
+ await client.batch.addTasks(streamingJob.job_id, [{ url: "https://example.com/3" }]);
+ // Or close it as part of the last addTasks() call instead of a separate request:
+ // await client.batch.addTasks(streamingJob.job_id, [...], { lastBatch: true });
+ await client.batch.closeJob(streamingJob.job_id);
+
+ // Poll for progress and page through results once it's done.
+ const status = await client.batch.getJob(job.job_id);
+ const { results, next_cursor } = await client.batch.getResults(job.job_id);
+
+ // Retry only the tasks that failed, inheriting the rest from the previous run.
+ await client.batch.rerun(job.job_id, { status: "failed" });
+})();
+```
+
+`client.batch` also exposes `listJobs()`, `deleteJob()`, `stopRun()`, `rerun()`, `listRuns()`, `getRun()`, `deleteRun()`, and `getTaskContent()` (returns the scraped page's raw content as a string, not JSON — the endpoint can return HTML or plain text depending on what the target page served). Scheduling, webhook config, HMAC key rotation, CSV task uploads, and results exports aren't wrapped yet — call the [Batch API](https://docs.zenrows.com) directly for those.
+
+The batch client (`ZenRowsBatchClient`) also works standalone, without a `ZenRows` instance — matching the Go and Python SDKs' batch clients:
+
+```javascript
+const { ZenRowsBatchClient } = require("zenrows");
+
+const batch = new ZenRowsBatchClient(apiKey, { baseURL: "https://async.api.zenrows.com/v1" }); // baseURL is optional
+```
+
### Concurrency
To limit the concurrency, it uses [fastq](https://github.com/mcollina/fastq), which will simultaneously send a maximum of requests. The concurrency is determined by the plan you are in, so take a look at the [pricing](https://www.zenrows.com/pricing) and set it accordingly. Take into account that each client instance will have its own limit, meaning that two different scripts will not share it, and 429 (Too Many Requests) errors might arise.
@@ -155,7 +228,7 @@ const apiKey = "YOUR-API-KEY";
// ...
];
- const promises = urls.map((url) => client.get(url));
+ const promises = urls.map((url) => client.fetch(url));
const results = await Promise.allSettled(promises);
console.log(results);
@@ -182,7 +255,7 @@ const apiKey = "YOUR-API-KEY";
`Promise.allSettled()` does not narrow the type of the array elements in the callback function. This means that you will need to cast the type of the array elements to `PromiseSettledResult` to access the `status` and `value` properties.
```typescript
-const promises = urls.map((url) => client.get(url));
+const promises = urls.map((url) => client.fetch(url));
const results = await Promise.allSettled(promises);
diff --git a/package.json b/package.json
index f8d3841..9b16cc8 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"@biomejs/biome": "1.8.1",
"@types/async-retry": "^1.4.9",
"@types/node": "^20.19.39",
+ "@vitest/coverage-v8": "1.6.1",
"msw": "^2.13.4",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cbc6f45..9d39208 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,22 +23,49 @@ importers:
version: 1.4.9
'@types/node':
specifier: ^20.19.39
- version: 20.19.39
+ version: 20.19.43
+ '@vitest/coverage-v8':
+ specifier: 1.6.1
+ version: 1.6.1(vitest@3.2.7(@types/node@20.19.43)(msw@2.15.0(@types/node@20.19.43)(typescript@5.9.3)))
msw:
specifier: ^2.13.4
- version: 2.13.4(@types/node@20.19.39)(typescript@5.9.3)
+ version: 2.15.0(@types/node@20.19.43)(typescript@5.9.3)
tsup:
specifier: ^8.5.1
- version: 8.5.1(postcss@8.5.15)(typescript@5.9.3)(yaml@2.4.5)
+ version: 8.5.1(postcss@8.5.26)(typescript@5.9.3)
typescript:
specifier: ^5.9.3
version: 5.9.3
vitest:
specifier: ^3.2.6
- version: 3.2.6(@types/node@20.19.39)(msw@2.13.4(@types/node@20.19.39)(typescript@5.9.3))(yaml@2.4.5)
+ version: 3.2.7(@types/node@20.19.43)(msw@2.15.0(@types/node@20.19.43)(typescript@5.9.3))
packages:
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.8':
+ resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@bcoe/v8-coverage@0.2.3':
+ resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+
'@biomejs/biome@1.8.1':
resolution: {integrity: sha512-fQXGfvq6DIXem12dGQCM2tNF+vsNHH1qs3C7WeOu75Pd0trduoTmoO7G4ntLJ2qDs5wuw981H+cxQhi1uHnAtA==}
engines: {node: '>=14.21.3'}
@@ -98,191 +125,351 @@ packages:
cpu: [ppc64]
os: [aix]
+ '@esbuild/aix-ppc64@0.28.2':
+ resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
'@esbuild/android-arm64@0.27.7':
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
+ '@esbuild/android-arm64@0.28.2':
+ resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
'@esbuild/android-arm@0.27.7':
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
+ '@esbuild/android-arm@0.28.2':
+ resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
'@esbuild/android-x64@0.27.7':
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
+ '@esbuild/android-x64@0.28.2':
+ resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
'@esbuild/darwin-arm64@0.27.7':
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
+ '@esbuild/darwin-arm64@0.28.2':
+ resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
'@esbuild/darwin-x64@0.27.7':
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
+ '@esbuild/darwin-x64@0.28.2':
+ resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
'@esbuild/freebsd-arm64@0.27.7':
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
+ '@esbuild/freebsd-arm64@0.28.2':
+ resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
'@esbuild/freebsd-x64@0.27.7':
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
+ '@esbuild/freebsd-x64@0.28.2':
+ resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
'@esbuild/linux-arm64@0.27.7':
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
+ '@esbuild/linux-arm64@0.28.2':
+ resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
'@esbuild/linux-arm@0.27.7':
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
+ '@esbuild/linux-arm@0.28.2':
+ resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
'@esbuild/linux-ia32@0.27.7':
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
+ '@esbuild/linux-ia32@0.28.2':
+ resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
'@esbuild/linux-loong64@0.27.7':
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
+ '@esbuild/linux-loong64@0.28.2':
+ resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
'@esbuild/linux-mips64el@0.27.7':
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
+ '@esbuild/linux-mips64el@0.28.2':
+ resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
'@esbuild/linux-ppc64@0.27.7':
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
+ '@esbuild/linux-ppc64@0.28.2':
+ resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
'@esbuild/linux-riscv64@0.27.7':
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
+ '@esbuild/linux-riscv64@0.28.2':
+ resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
'@esbuild/linux-s390x@0.27.7':
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
+ '@esbuild/linux-s390x@0.28.2':
+ resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
'@esbuild/linux-x64@0.27.7':
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
+ '@esbuild/linux-x64@0.28.2':
+ resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
'@esbuild/netbsd-arm64@0.27.7':
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
+ '@esbuild/netbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
'@esbuild/netbsd-x64@0.27.7':
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
+ '@esbuild/netbsd-x64@0.28.2':
+ resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
'@esbuild/openbsd-arm64@0.27.7':
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
+ '@esbuild/openbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
'@esbuild/openbsd-x64@0.27.7':
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
+ '@esbuild/openbsd-x64@0.28.2':
+ resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
'@esbuild/openharmony-arm64@0.27.7':
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
+ '@esbuild/openharmony-arm64@0.28.2':
+ resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
'@esbuild/sunos-x64@0.27.7':
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
+ '@esbuild/sunos-x64@0.28.2':
+ resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
'@esbuild/win32-arm64@0.27.7':
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
+ '@esbuild/win32-arm64@0.28.2':
+ resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
'@esbuild/win32-ia32@0.27.7':
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
+ '@esbuild/win32-ia32@0.28.2':
+ resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
'@esbuild/win32-x64@0.27.7':
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
- '@inquirer/ansi@2.0.5':
- resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
- engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ '@esbuild/win32-x64@0.28.2':
+ resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@inquirer/ansi@2.0.7':
+ resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
- '@inquirer/confirm@6.0.12':
- resolution: {integrity: sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==}
- engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ '@inquirer/confirm@6.2.0':
+ resolution: {integrity: sha512-SKXarWrYhtpqOEctf9XGCGy29QjsvJAM0Aq9ZR9z4Ns94OmpqudOly+aSEfNqUf9SwsQaUgY9+Z8hyzG0xX8fw==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
- '@inquirer/core@11.1.9':
- resolution: {integrity: sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==}
- engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ '@inquirer/core@12.0.0':
+ resolution: {integrity: sha512-+nnvFEXIB08CZNVXpvW3B+zHW96QXvGUjNKJ8NJIPqAZi5Kd4WhYt2S3C234ReepG1qw2HOlEUbjYVHBowXObA==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
- '@inquirer/figures@2.0.5':
- resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==}
- engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ '@inquirer/figures@2.0.8':
+ resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
- '@inquirer/type@4.0.5':
- resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==}
- engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
+ '@inquirer/type@4.0.7':
+ resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==}
+ engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
+ '@istanbuljs/schema@0.1.6':
+ resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
+ engines: {node: '>=8'}
+
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -296,10 +483,16 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
- '@mswjs/interceptors@0.41.4':
- resolution: {integrity: sha512-3B9EinUkrdOUGYzHRzRWSXunQ4YFGboJnyLNRwEJWEde+j8fNhPUHvrN1E3g1DU/iS/s8JQrMNVe+S7AHHVs0w==}
+ '@mswjs/interceptors@0.41.9':
+ resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==}
engines: {node: '>=18'}
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
+ engines: {node: ^22.20 || ^24.12 || >=25}
+ cpu: [x64]
+ os: [linux]
+
'@open-draft/deferred-promise@2.2.0':
resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
@@ -312,253 +505,128 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
- '@rollup/rollup-android-arm-eabi@4.60.2':
- resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==}
- cpu: [arm]
- os: [android]
-
- '@rollup/rollup-android-arm-eabi@4.61.1':
- resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==}
+ '@rollup/rollup-android-arm-eabi@4.62.4':
+ resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==}
cpu: [arm]
os: [android]
- '@rollup/rollup-android-arm64@4.60.2':
- resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==}
- cpu: [arm64]
- os: [android]
-
- '@rollup/rollup-android-arm64@4.61.1':
- resolution: {integrity: sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==}
+ '@rollup/rollup-android-arm64@4.62.4':
+ resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==}
cpu: [arm64]
os: [android]
- '@rollup/rollup-darwin-arm64@4.60.2':
- resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==}
+ '@rollup/rollup-darwin-arm64@4.62.4':
+ resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==}
cpu: [arm64]
os: [darwin]
- '@rollup/rollup-darwin-arm64@4.61.1':
- resolution: {integrity: sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==}
- cpu: [arm64]
- os: [darwin]
-
- '@rollup/rollup-darwin-x64@4.60.2':
- resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==}
- cpu: [x64]
- os: [darwin]
-
- '@rollup/rollup-darwin-x64@4.61.1':
- resolution: {integrity: sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==}
+ '@rollup/rollup-darwin-x64@4.62.4':
+ resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==}
cpu: [x64]
os: [darwin]
- '@rollup/rollup-freebsd-arm64@4.60.2':
- resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==}
+ '@rollup/rollup-freebsd-arm64@4.62.4':
+ resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==}
cpu: [arm64]
os: [freebsd]
- '@rollup/rollup-freebsd-arm64@4.61.1':
- resolution: {integrity: sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==}
- cpu: [arm64]
- os: [freebsd]
-
- '@rollup/rollup-freebsd-x64@4.60.2':
- resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==}
+ '@rollup/rollup-freebsd-x64@4.62.4':
+ resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==}
cpu: [x64]
os: [freebsd]
- '@rollup/rollup-freebsd-x64@4.61.1':
- resolution: {integrity: sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==}
- cpu: [x64]
- os: [freebsd]
-
- '@rollup/rollup-linux-arm-gnueabihf@4.60.2':
- resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==}
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.4':
+ resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==}
cpu: [arm]
os: [linux]
- '@rollup/rollup-linux-arm-gnueabihf@4.61.1':
- resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==}
+ '@rollup/rollup-linux-arm-musleabihf@4.62.4':
+ resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==}
cpu: [arm]
os: [linux]
- '@rollup/rollup-linux-arm-musleabihf@4.60.2':
- resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==}
- cpu: [arm]
- os: [linux]
-
- '@rollup/rollup-linux-arm-musleabihf@4.61.1':
- resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==}
- cpu: [arm]
- os: [linux]
-
- '@rollup/rollup-linux-arm64-gnu@4.60.2':
- resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==}
- cpu: [arm64]
- os: [linux]
-
- '@rollup/rollup-linux-arm64-gnu@4.61.1':
- resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==}
+ '@rollup/rollup-linux-arm64-gnu@4.62.4':
+ resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==}
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-arm64-musl@4.60.2':
- resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==}
+ '@rollup/rollup-linux-arm64-musl@4.62.4':
+ resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==}
cpu: [arm64]
os: [linux]
- '@rollup/rollup-linux-arm64-musl@4.61.1':
- resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==}
- cpu: [arm64]
- os: [linux]
-
- '@rollup/rollup-linux-loong64-gnu@4.60.2':
- resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==}
+ '@rollup/rollup-linux-loong64-gnu@4.62.4':
+ resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==}
cpu: [loong64]
os: [linux]
- '@rollup/rollup-linux-loong64-gnu@4.61.1':
- resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==}
+ '@rollup/rollup-linux-loong64-musl@4.62.4':
+ resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==}
cpu: [loong64]
os: [linux]
- '@rollup/rollup-linux-loong64-musl@4.60.2':
- resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==}
- cpu: [loong64]
- os: [linux]
-
- '@rollup/rollup-linux-loong64-musl@4.61.1':
- resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==}
- cpu: [loong64]
- os: [linux]
-
- '@rollup/rollup-linux-ppc64-gnu@4.60.2':
- resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==}
- cpu: [ppc64]
- os: [linux]
-
- '@rollup/rollup-linux-ppc64-gnu@4.61.1':
- resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==}
- cpu: [ppc64]
- os: [linux]
-
- '@rollup/rollup-linux-ppc64-musl@4.60.2':
- resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==}
+ '@rollup/rollup-linux-ppc64-gnu@4.62.4':
+ resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==}
cpu: [ppc64]
os: [linux]
- '@rollup/rollup-linux-ppc64-musl@4.61.1':
- resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==}
+ '@rollup/rollup-linux-ppc64-musl@4.62.4':
+ resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==}
cpu: [ppc64]
os: [linux]
- '@rollup/rollup-linux-riscv64-gnu@4.60.2':
- resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==}
- cpu: [riscv64]
- os: [linux]
-
- '@rollup/rollup-linux-riscv64-gnu@4.61.1':
- resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==}
+ '@rollup/rollup-linux-riscv64-gnu@4.62.4':
+ resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==}
cpu: [riscv64]
os: [linux]
- '@rollup/rollup-linux-riscv64-musl@4.60.2':
- resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==}
+ '@rollup/rollup-linux-riscv64-musl@4.62.4':
+ resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==}
cpu: [riscv64]
os: [linux]
- '@rollup/rollup-linux-riscv64-musl@4.61.1':
- resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==}
- cpu: [riscv64]
- os: [linux]
-
- '@rollup/rollup-linux-s390x-gnu@4.60.2':
- resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==}
- cpu: [s390x]
- os: [linux]
-
- '@rollup/rollup-linux-s390x-gnu@4.61.1':
- resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==}
+ '@rollup/rollup-linux-s390x-gnu@4.62.4':
+ resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==}
cpu: [s390x]
os: [linux]
- '@rollup/rollup-linux-x64-gnu@4.60.2':
- resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==}
+ '@rollup/rollup-linux-x64-gnu@4.62.4':
+ resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==}
cpu: [x64]
os: [linux]
- '@rollup/rollup-linux-x64-gnu@4.61.1':
- resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==}
+ '@rollup/rollup-linux-x64-musl@4.62.4':
+ resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==}
cpu: [x64]
os: [linux]
- '@rollup/rollup-linux-x64-musl@4.60.2':
- resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==}
- cpu: [x64]
- os: [linux]
-
- '@rollup/rollup-linux-x64-musl@4.61.1':
- resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==}
- cpu: [x64]
- os: [linux]
-
- '@rollup/rollup-openbsd-x64@4.60.2':
- resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==}
+ '@rollup/rollup-openbsd-x64@4.62.4':
+ resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==}
cpu: [x64]
os: [openbsd]
- '@rollup/rollup-openbsd-x64@4.61.1':
- resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==}
- cpu: [x64]
- os: [openbsd]
-
- '@rollup/rollup-openharmony-arm64@4.60.2':
- resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==}
- cpu: [arm64]
- os: [openharmony]
-
- '@rollup/rollup-openharmony-arm64@4.61.1':
- resolution: {integrity: sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==}
+ '@rollup/rollup-openharmony-arm64@4.62.4':
+ resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==}
cpu: [arm64]
os: [openharmony]
- '@rollup/rollup-win32-arm64-msvc@4.60.2':
- resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==}
- cpu: [arm64]
- os: [win32]
-
- '@rollup/rollup-win32-arm64-msvc@4.61.1':
- resolution: {integrity: sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==}
+ '@rollup/rollup-win32-arm64-msvc@4.62.4':
+ resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==}
cpu: [arm64]
os: [win32]
- '@rollup/rollup-win32-ia32-msvc@4.60.2':
- resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==}
+ '@rollup/rollup-win32-ia32-msvc@4.62.4':
+ resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==}
cpu: [ia32]
os: [win32]
- '@rollup/rollup-win32-ia32-msvc@4.61.1':
- resolution: {integrity: sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==}
- cpu: [ia32]
- os: [win32]
-
- '@rollup/rollup-win32-x64-gnu@4.60.2':
- resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==}
+ '@rollup/rollup-win32-x64-gnu@4.62.4':
+ resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==}
cpu: [x64]
os: [win32]
- '@rollup/rollup-win32-x64-gnu@4.61.1':
- resolution: {integrity: sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==}
- cpu: [x64]
- os: [win32]
-
- '@rollup/rollup-win32-x64-msvc@4.60.2':
- resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==}
- cpu: [x64]
- os: [win32]
-
- '@rollup/rollup-win32-x64-msvc@4.61.1':
- resolution: {integrity: sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==}
+ '@rollup/rollup-win32-x64-msvc@4.62.4':
+ resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==}
cpu: [x64]
os: [win32]
@@ -571,14 +639,11 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
- '@types/estree@1.0.8':
- resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
-
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
- '@types/node@20.19.39':
- resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==}
+ '@types/node@20.19.43':
+ resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==}
'@types/retry@0.12.5':
resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==}
@@ -589,11 +654,16 @@ packages:
'@types/statuses@2.0.6':
resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==}
- '@vitest/expect@3.2.6':
- resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==}
+ '@vitest/coverage-v8@1.6.1':
+ resolution: {integrity: sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==}
+ peerDependencies:
+ vitest: 1.6.1
+
+ '@vitest/expect@3.2.7':
+ resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==}
- '@vitest/mocker@3.2.6':
- resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==}
+ '@vitest/mocker@3.2.7':
+ resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
@@ -603,23 +673,23 @@ packages:
vite:
optional: true
- '@vitest/pretty-format@3.2.6':
- resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==}
+ '@vitest/pretty-format@3.2.7':
+ resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==}
- '@vitest/runner@3.2.6':
- resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==}
+ '@vitest/runner@3.2.7':
+ resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==}
- '@vitest/snapshot@3.2.6':
- resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==}
+ '@vitest/snapshot@3.2.7':
+ resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==}
- '@vitest/spy@3.2.6':
- resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==}
+ '@vitest/spy@3.2.7':
+ resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==}
- '@vitest/utils@3.2.6':
- resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==}
+ '@vitest/utils@3.2.7':
+ resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==}
- acorn@8.16.0:
- resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
+ acorn@8.18.0:
+ resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
engines: {node: '>=0.4.0'}
hasBin: true
@@ -638,6 +708,12 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@1.1.18:
+ resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
+
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -679,6 +755,9 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
@@ -714,6 +793,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ esbuild@0.28.2:
+ resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -721,8 +805,8 @@ packages:
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
- expect-type@1.3.0:
- resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ expect-type@1.4.0:
+ resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
engines: {node: '>=12.0.0'}
fast-string-truncated-width@3.0.3:
@@ -731,8 +815,8 @@ packages:
fast-string-width@3.0.2:
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
- fast-wrap-ansi@0.2.0:
- resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==}
+ fast-wrap-ansi@0.2.2:
+ resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
@@ -752,6 +836,9 @@ packages:
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
+ fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -761,13 +848,31 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
- graphql@16.13.2:
- resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==}
+ glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
+
+ graphql@16.14.2:
+ resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
headers-polyfill@5.0.1:
resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==}
+ html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
+ inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
@@ -775,6 +880,22 @@ packages:
is-node-process@1.2.0:
resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==}
+ istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+
+ istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+
+ istanbul-lib-source-maps@5.0.6:
+ resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
+ engines: {node: '>=10'}
+
+ istanbul-reports@3.2.0:
+ resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
+ engines: {node: '>=8'}
+
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
@@ -799,14 +920,24 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+ magicast@0.3.5:
+ resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+
+ make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+
+ minimatch@3.1.5:
+ resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+
mlly@1.8.2:
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
- msw@2.13.4:
- resolution: {integrity: sha512-fPlKBeFe+8rpcyR3umUmmHuNwu6gc6T3STvkgEa9WDX/HEgal9wDeflpCUAIRtmvaLZM2igfI5y1bZ9G5J26KA==}
+ msw@2.15.0:
+ resolution: {integrity: sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
@@ -822,8 +953,8 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
- nanoid@3.3.12:
- resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -831,9 +962,16 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+ path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
@@ -847,8 +985,8 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
- picomatch@4.0.4:
- resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ picomatch@4.0.5:
+ resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
pirates@4.0.7:
@@ -876,8 +1014,8 @@ packages:
yaml:
optional: true
- postcss@8.5.15:
- resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
+ postcss@8.5.26:
+ resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
readdirp@4.1.2:
@@ -892,25 +1030,25 @@ packages:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
- rettime@0.11.8:
- resolution: {integrity: sha512-0fERGXktJTyJ+h8fBEiPxHPEFOu0h15JY7JtwrOVqR5K+vb99ho6IyOo7ekLS3h4sJCzIDy4VWKIbZUfe9njmg==}
+ rettime@0.11.11:
+ resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
- rollup@4.60.2:
- resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==}
+ rollup@4.62.4:
+ resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
- rollup@4.61.1:
- resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==}
- engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
hasBin: true
- set-cookie-parser@3.1.0:
- resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==}
+ set-cookie-parser@3.1.2:
+ resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -948,6 +1086,9 @@ packages:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
+ strip-literal@2.1.1:
+ resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
+
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
@@ -956,10 +1097,18 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
+ test-exclude@6.0.0:
+ resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
+ engines: {node: '>=8'}
+
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@@ -973,10 +1122,6 @@ packages:
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
- tinyglobby@0.2.16:
- resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
- engines: {node: '>=12.0.0'}
-
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
@@ -993,15 +1138,15 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
- tldts-core@7.0.28:
- resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==}
+ tldts-core@7.4.10:
+ resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==}
- tldts@7.0.28:
- resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==}
+ tldts@7.4.10:
+ resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==}
hasBin: true
- tough-cookie@6.0.1:
- resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
+ tough-cookie@6.0.2:
+ resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==}
engines: {node: '>=16'}
tree-kill@1.2.2:
@@ -1030,8 +1175,8 @@ packages:
typescript:
optional: true
- type-fest@5.6.0:
- resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
+ type-fest@5.8.0:
+ resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==}
engines: {node: '>=20'}
typescript@5.9.3:
@@ -1039,8 +1184,8 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
- ufo@1.6.3:
- resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
+ ufo@1.6.4:
+ resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -1053,8 +1198,8 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
- vite@7.3.5:
- resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==}
+ vite@7.3.6:
+ resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -1093,16 +1238,16 @@ packages:
yaml:
optional: true
- vitest@3.2.6:
- resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==}
+ vitest@3.2.7:
+ resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
- '@vitest/browser': 3.2.6
- '@vitest/ui': 3.2.6
+ '@vitest/browser': 3.2.7
+ '@vitest/ui': 3.2.7
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
@@ -1130,25 +1275,43 @@ packages:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
- yaml@2.4.5:
- resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==}
- engines: {node: '>= 14'}
- hasBin: true
-
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
- yargs@17.7.2:
- resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ yargs@17.7.3:
+ resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'}
snapshots:
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/parser@7.29.8':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@bcoe/v8-coverage@0.2.3': {}
+
'@biomejs/biome@1.8.1':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.8.1
@@ -1187,107 +1350,187 @@ snapshots:
'@esbuild/aix-ppc64@0.27.7':
optional: true
+ '@esbuild/aix-ppc64@0.28.2':
+ optional: true
+
'@esbuild/android-arm64@0.27.7':
optional: true
+ '@esbuild/android-arm64@0.28.2':
+ optional: true
+
'@esbuild/android-arm@0.27.7':
optional: true
+ '@esbuild/android-arm@0.28.2':
+ optional: true
+
'@esbuild/android-x64@0.27.7':
optional: true
+ '@esbuild/android-x64@0.28.2':
+ optional: true
+
'@esbuild/darwin-arm64@0.27.7':
optional: true
+ '@esbuild/darwin-arm64@0.28.2':
+ optional: true
+
'@esbuild/darwin-x64@0.27.7':
optional: true
+ '@esbuild/darwin-x64@0.28.2':
+ optional: true
+
'@esbuild/freebsd-arm64@0.27.7':
optional: true
+ '@esbuild/freebsd-arm64@0.28.2':
+ optional: true
+
'@esbuild/freebsd-x64@0.27.7':
optional: true
+ '@esbuild/freebsd-x64@0.28.2':
+ optional: true
+
'@esbuild/linux-arm64@0.27.7':
optional: true
+ '@esbuild/linux-arm64@0.28.2':
+ optional: true
+
'@esbuild/linux-arm@0.27.7':
optional: true
+ '@esbuild/linux-arm@0.28.2':
+ optional: true
+
'@esbuild/linux-ia32@0.27.7':
optional: true
+ '@esbuild/linux-ia32@0.28.2':
+ optional: true
+
'@esbuild/linux-loong64@0.27.7':
optional: true
+ '@esbuild/linux-loong64@0.28.2':
+ optional: true
+
'@esbuild/linux-mips64el@0.27.7':
optional: true
+ '@esbuild/linux-mips64el@0.28.2':
+ optional: true
+
'@esbuild/linux-ppc64@0.27.7':
optional: true
+ '@esbuild/linux-ppc64@0.28.2':
+ optional: true
+
'@esbuild/linux-riscv64@0.27.7':
optional: true
+ '@esbuild/linux-riscv64@0.28.2':
+ optional: true
+
'@esbuild/linux-s390x@0.27.7':
optional: true
+ '@esbuild/linux-s390x@0.28.2':
+ optional: true
+
'@esbuild/linux-x64@0.27.7':
optional: true
+ '@esbuild/linux-x64@0.28.2':
+ optional: true
+
'@esbuild/netbsd-arm64@0.27.7':
optional: true
+ '@esbuild/netbsd-arm64@0.28.2':
+ optional: true
+
'@esbuild/netbsd-x64@0.27.7':
optional: true
+ '@esbuild/netbsd-x64@0.28.2':
+ optional: true
+
'@esbuild/openbsd-arm64@0.27.7':
optional: true
+ '@esbuild/openbsd-arm64@0.28.2':
+ optional: true
+
'@esbuild/openbsd-x64@0.27.7':
optional: true
+ '@esbuild/openbsd-x64@0.28.2':
+ optional: true
+
'@esbuild/openharmony-arm64@0.27.7':
optional: true
+ '@esbuild/openharmony-arm64@0.28.2':
+ optional: true
+
'@esbuild/sunos-x64@0.27.7':
optional: true
+ '@esbuild/sunos-x64@0.28.2':
+ optional: true
+
'@esbuild/win32-arm64@0.27.7':
optional: true
+ '@esbuild/win32-arm64@0.28.2':
+ optional: true
+
'@esbuild/win32-ia32@0.27.7':
optional: true
+ '@esbuild/win32-ia32@0.28.2':
+ optional: true
+
'@esbuild/win32-x64@0.27.7':
optional: true
- '@inquirer/ansi@2.0.5': {}
+ '@esbuild/win32-x64@0.28.2':
+ optional: true
+
+ '@inquirer/ansi@2.0.7': {}
- '@inquirer/confirm@6.0.12(@types/node@20.19.39)':
+ '@inquirer/confirm@6.2.0(@types/node@20.19.43)':
dependencies:
- '@inquirer/core': 11.1.9(@types/node@20.19.39)
- '@inquirer/type': 4.0.5(@types/node@20.19.39)
+ '@inquirer/core': 12.0.0(@types/node@20.19.43)
+ '@inquirer/type': 4.0.7(@types/node@20.19.43)
optionalDependencies:
- '@types/node': 20.19.39
+ '@types/node': 20.19.43
- '@inquirer/core@11.1.9(@types/node@20.19.39)':
+ '@inquirer/core@12.0.0(@types/node@20.19.43)':
dependencies:
- '@inquirer/ansi': 2.0.5
- '@inquirer/figures': 2.0.5
- '@inquirer/type': 4.0.5(@types/node@20.19.39)
+ '@inquirer/ansi': 2.0.7
+ '@inquirer/figures': 2.0.8
+ '@inquirer/type': 4.0.7(@types/node@20.19.43)
cli-width: 4.1.0
- fast-wrap-ansi: 0.2.0
+ fast-wrap-ansi: 0.2.2
mute-stream: 3.0.0
signal-exit: 4.1.0
optionalDependencies:
- '@types/node': 20.19.39
+ '@types/node': 20.19.43
- '@inquirer/figures@2.0.5': {}
+ '@inquirer/figures@2.0.8': {}
- '@inquirer/type@4.0.5(@types/node@20.19.39)':
+ '@inquirer/type@4.0.7(@types/node@20.19.43)':
optionalDependencies:
- '@types/node': 20.19.39
+ '@types/node': 20.19.43
+
+ '@istanbuljs/schema@0.1.6': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
@@ -1303,7 +1546,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
- '@mswjs/interceptors@0.41.4':
+ '@mswjs/interceptors@0.41.9':
dependencies:
'@open-draft/deferred-promise': 2.2.0
'@open-draft/logger': 0.3.0
@@ -1312,6 +1555,9 @@ snapshots:
outvariant: 1.4.3
strict-event-emitter: 0.5.1
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ optional: true
+
'@open-draft/deferred-promise@2.2.0': {}
'@open-draft/deferred-promise@3.0.0': {}
@@ -1323,154 +1569,79 @@ snapshots:
'@open-draft/until@2.1.0': {}
- '@rollup/rollup-android-arm-eabi@4.60.2':
- optional: true
-
- '@rollup/rollup-android-arm-eabi@4.61.1':
- optional: true
-
- '@rollup/rollup-android-arm64@4.60.2':
- optional: true
-
- '@rollup/rollup-android-arm64@4.61.1':
- optional: true
-
- '@rollup/rollup-darwin-arm64@4.60.2':
- optional: true
-
- '@rollup/rollup-darwin-arm64@4.61.1':
+ '@rollup/rollup-android-arm-eabi@4.62.4':
optional: true
- '@rollup/rollup-darwin-x64@4.60.2':
+ '@rollup/rollup-android-arm64@4.62.4':
optional: true
- '@rollup/rollup-darwin-x64@4.61.1':
+ '@rollup/rollup-darwin-arm64@4.62.4':
optional: true
- '@rollup/rollup-freebsd-arm64@4.60.2':
+ '@rollup/rollup-darwin-x64@4.62.4':
optional: true
- '@rollup/rollup-freebsd-arm64@4.61.1':
+ '@rollup/rollup-freebsd-arm64@4.62.4':
optional: true
- '@rollup/rollup-freebsd-x64@4.60.2':
+ '@rollup/rollup-freebsd-x64@4.62.4':
optional: true
- '@rollup/rollup-freebsd-x64@4.61.1':
+ '@rollup/rollup-linux-arm-gnueabihf@4.62.4':
optional: true
- '@rollup/rollup-linux-arm-gnueabihf@4.60.2':
+ '@rollup/rollup-linux-arm-musleabihf@4.62.4':
optional: true
- '@rollup/rollup-linux-arm-gnueabihf@4.61.1':
+ '@rollup/rollup-linux-arm64-gnu@4.62.4':
optional: true
- '@rollup/rollup-linux-arm-musleabihf@4.60.2':
+ '@rollup/rollup-linux-arm64-musl@4.62.4':
optional: true
- '@rollup/rollup-linux-arm-musleabihf@4.61.1':
+ '@rollup/rollup-linux-loong64-gnu@4.62.4':
optional: true
- '@rollup/rollup-linux-arm64-gnu@4.60.2':
+ '@rollup/rollup-linux-loong64-musl@4.62.4':
optional: true
- '@rollup/rollup-linux-arm64-gnu@4.61.1':
+ '@rollup/rollup-linux-ppc64-gnu@4.62.4':
optional: true
- '@rollup/rollup-linux-arm64-musl@4.60.2':
+ '@rollup/rollup-linux-ppc64-musl@4.62.4':
optional: true
- '@rollup/rollup-linux-arm64-musl@4.61.1':
+ '@rollup/rollup-linux-riscv64-gnu@4.62.4':
optional: true
- '@rollup/rollup-linux-loong64-gnu@4.60.2':
+ '@rollup/rollup-linux-riscv64-musl@4.62.4':
optional: true
- '@rollup/rollup-linux-loong64-gnu@4.61.1':
+ '@rollup/rollup-linux-s390x-gnu@4.62.4':
optional: true
- '@rollup/rollup-linux-loong64-musl@4.60.2':
+ '@rollup/rollup-linux-x64-gnu@4.62.4':
optional: true
- '@rollup/rollup-linux-loong64-musl@4.61.1':
+ '@rollup/rollup-linux-x64-musl@4.62.4':
optional: true
- '@rollup/rollup-linux-ppc64-gnu@4.60.2':
+ '@rollup/rollup-openbsd-x64@4.62.4':
optional: true
- '@rollup/rollup-linux-ppc64-gnu@4.61.1':
+ '@rollup/rollup-openharmony-arm64@4.62.4':
optional: true
- '@rollup/rollup-linux-ppc64-musl@4.60.2':
+ '@rollup/rollup-win32-arm64-msvc@4.62.4':
optional: true
- '@rollup/rollup-linux-ppc64-musl@4.61.1':
+ '@rollup/rollup-win32-ia32-msvc@4.62.4':
optional: true
- '@rollup/rollup-linux-riscv64-gnu@4.60.2':
+ '@rollup/rollup-win32-x64-gnu@4.62.4':
optional: true
- '@rollup/rollup-linux-riscv64-gnu@4.61.1':
- optional: true
-
- '@rollup/rollup-linux-riscv64-musl@4.60.2':
- optional: true
-
- '@rollup/rollup-linux-riscv64-musl@4.61.1':
- optional: true
-
- '@rollup/rollup-linux-s390x-gnu@4.60.2':
- optional: true
-
- '@rollup/rollup-linux-s390x-gnu@4.61.1':
- optional: true
-
- '@rollup/rollup-linux-x64-gnu@4.60.2':
- optional: true
-
- '@rollup/rollup-linux-x64-gnu@4.61.1':
- optional: true
-
- '@rollup/rollup-linux-x64-musl@4.60.2':
- optional: true
-
- '@rollup/rollup-linux-x64-musl@4.61.1':
- optional: true
-
- '@rollup/rollup-openbsd-x64@4.60.2':
- optional: true
-
- '@rollup/rollup-openbsd-x64@4.61.1':
- optional: true
-
- '@rollup/rollup-openharmony-arm64@4.60.2':
- optional: true
-
- '@rollup/rollup-openharmony-arm64@4.61.1':
- optional: true
-
- '@rollup/rollup-win32-arm64-msvc@4.60.2':
- optional: true
-
- '@rollup/rollup-win32-arm64-msvc@4.61.1':
- optional: true
-
- '@rollup/rollup-win32-ia32-msvc@4.60.2':
- optional: true
-
- '@rollup/rollup-win32-ia32-msvc@4.61.1':
- optional: true
-
- '@rollup/rollup-win32-x64-gnu@4.60.2':
- optional: true
-
- '@rollup/rollup-win32-x64-gnu@4.61.1':
- optional: true
-
- '@rollup/rollup-win32-x64-msvc@4.60.2':
- optional: true
-
- '@rollup/rollup-win32-x64-msvc@4.61.1':
+ '@rollup/rollup-win32-x64-msvc@4.62.4':
optional: true
'@types/async-retry@1.4.9':
@@ -1484,11 +1655,9 @@ snapshots:
'@types/deep-eql@4.0.2': {}
- '@types/estree@1.0.8': {}
-
'@types/estree@1.0.9': {}
- '@types/node@20.19.39':
+ '@types/node@20.19.43':
dependencies:
undici-types: 6.21.0
@@ -1496,54 +1665,73 @@ snapshots:
'@types/set-cookie-parser@2.4.10':
dependencies:
- '@types/node': 20.19.39
+ '@types/node': 20.19.43
'@types/statuses@2.0.6': {}
- '@vitest/expect@3.2.6':
+ '@vitest/coverage-v8@1.6.1(vitest@3.2.7(@types/node@20.19.43)(msw@2.15.0(@types/node@20.19.43)(typescript@5.9.3)))':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@bcoe/v8-coverage': 0.2.3
+ debug: 4.4.3
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-lib-source-maps: 5.0.6
+ istanbul-reports: 3.2.0
+ magic-string: 0.30.21
+ magicast: 0.3.5
+ picocolors: 1.1.1
+ std-env: 3.10.0
+ strip-literal: 2.1.1
+ test-exclude: 6.0.0
+ vitest: 3.2.7(@types/node@20.19.43)(msw@2.15.0(@types/node@20.19.43)(typescript@5.9.3))
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitest/expect@3.2.7':
dependencies:
'@types/chai': 5.2.3
- '@vitest/spy': 3.2.6
- '@vitest/utils': 3.2.6
+ '@vitest/spy': 3.2.7
+ '@vitest/utils': 3.2.7
chai: 5.3.3
tinyrainbow: 2.0.0
- '@vitest/mocker@3.2.6(msw@2.13.4(@types/node@20.19.39)(typescript@5.9.3))(vite@7.3.5(@types/node@20.19.39)(yaml@2.4.5))':
+ '@vitest/mocker@3.2.7(msw@2.15.0(@types/node@20.19.43)(typescript@5.9.3))(vite@7.3.6(@types/node@20.19.43))':
dependencies:
- '@vitest/spy': 3.2.6
+ '@vitest/spy': 3.2.7
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- msw: 2.13.4(@types/node@20.19.39)(typescript@5.9.3)
- vite: 7.3.5(@types/node@20.19.39)(yaml@2.4.5)
+ msw: 2.15.0(@types/node@20.19.43)(typescript@5.9.3)
+ vite: 7.3.6(@types/node@20.19.43)
- '@vitest/pretty-format@3.2.6':
+ '@vitest/pretty-format@3.2.7':
dependencies:
tinyrainbow: 2.0.0
- '@vitest/runner@3.2.6':
+ '@vitest/runner@3.2.7':
dependencies:
- '@vitest/utils': 3.2.6
+ '@vitest/utils': 3.2.7
pathe: 2.0.3
strip-literal: 3.1.0
- '@vitest/snapshot@3.2.6':
+ '@vitest/snapshot@3.2.7':
dependencies:
- '@vitest/pretty-format': 3.2.6
+ '@vitest/pretty-format': 3.2.7
magic-string: 0.30.21
pathe: 2.0.3
- '@vitest/spy@3.2.6':
+ '@vitest/spy@3.2.7':
dependencies:
tinyspy: 4.0.4
- '@vitest/utils@3.2.6':
+ '@vitest/utils@3.2.7':
dependencies:
- '@vitest/pretty-format': 3.2.6
+ '@vitest/pretty-format': 3.2.7
loupe: 3.2.1
tinyrainbow: 2.0.0
- acorn@8.16.0: {}
+ acorn@8.18.0: {}
ansi-regex@5.0.1: {}
@@ -1555,6 +1743,13 @@ snapshots:
assertion-error@2.0.1: {}
+ balanced-match@1.0.2: {}
+
+ brace-expansion@1.1.18:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
bundle-require@5.1.0(esbuild@0.27.7):
dependencies:
esbuild: 0.27.7
@@ -1592,6 +1787,8 @@ snapshots:
commander@4.1.1: {}
+ concat-map@0.0.1: {}
+
confbox@0.1.8: {}
consola@3.4.2: {}
@@ -1637,13 +1834,42 @@ snapshots:
'@esbuild/win32-ia32': 0.27.7
'@esbuild/win32-x64': 0.27.7
+ esbuild@0.28.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.2
+ '@esbuild/android-arm': 0.28.2
+ '@esbuild/android-arm64': 0.28.2
+ '@esbuild/android-x64': 0.28.2
+ '@esbuild/darwin-arm64': 0.28.2
+ '@esbuild/darwin-x64': 0.28.2
+ '@esbuild/freebsd-arm64': 0.28.2
+ '@esbuild/freebsd-x64': 0.28.2
+ '@esbuild/linux-arm': 0.28.2
+ '@esbuild/linux-arm64': 0.28.2
+ '@esbuild/linux-ia32': 0.28.2
+ '@esbuild/linux-loong64': 0.28.2
+ '@esbuild/linux-mips64el': 0.28.2
+ '@esbuild/linux-ppc64': 0.28.2
+ '@esbuild/linux-riscv64': 0.28.2
+ '@esbuild/linux-s390x': 0.28.2
+ '@esbuild/linux-x64': 0.28.2
+ '@esbuild/netbsd-arm64': 0.28.2
+ '@esbuild/netbsd-x64': 0.28.2
+ '@esbuild/openbsd-arm64': 0.28.2
+ '@esbuild/openbsd-x64': 0.28.2
+ '@esbuild/openharmony-arm64': 0.28.2
+ '@esbuild/sunos-x64': 0.28.2
+ '@esbuild/win32-arm64': 0.28.2
+ '@esbuild/win32-ia32': 0.28.2
+ '@esbuild/win32-x64': 0.28.2
+
escalade@3.2.0: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.9
- expect-type@1.3.0: {}
+ expect-type@1.4.0: {}
fast-string-truncated-width@3.0.3: {}
@@ -1651,7 +1877,7 @@ snapshots:
dependencies:
fast-string-truncated-width: 3.0.3
- fast-wrap-ansi@0.2.0:
+ fast-wrap-ansi@0.2.2:
dependencies:
fast-string-width: 3.0.2
@@ -1659,9 +1885,9 @@ snapshots:
dependencies:
reusify: 1.1.0
- fdir@6.5.0(picomatch@4.0.4):
+ fdir@6.5.0(picomatch@4.0.5):
optionalDependencies:
- picomatch: 4.0.4
+ picomatch: 4.0.5
fetch-retry@6.0.0: {}
@@ -1669,24 +1895,67 @@ snapshots:
dependencies:
magic-string: 0.30.21
mlly: 1.8.2
- rollup: 4.60.2
+ rollup: 4.62.4
+
+ fs.realpath@1.0.0: {}
fsevents@2.3.3:
optional: true
get-caller-file@2.0.5: {}
- graphql@16.13.2: {}
+ glob@7.2.3:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.5
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
+ graphql@16.14.2: {}
+
+ has-flag@4.0.0: {}
headers-polyfill@5.0.1:
dependencies:
'@types/set-cookie-parser': 2.4.10
- set-cookie-parser: 3.1.0
+ set-cookie-parser: 3.1.2
+
+ html-escaper@2.0.2: {}
+
+ inflight@1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ inherits@2.0.4: {}
is-fullwidth-code-point@3.0.0: {}
is-node-process@1.2.0: {}
+ istanbul-lib-coverage@3.2.2: {}
+
+ istanbul-lib-report@3.0.1:
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+
+ istanbul-lib-source-maps@5.0.6:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ debug: 4.4.3
+ istanbul-lib-coverage: 3.2.2
+ transitivePeerDependencies:
+ - supports-color
+
+ istanbul-reports@3.2.0:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+
joycon@3.1.1: {}
js-tokens@9.0.1: {}
@@ -1703,35 +1972,49 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ magicast@0.3.5:
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+ source-map-js: 1.2.1
+
+ make-dir@4.0.0:
+ dependencies:
+ semver: 7.8.5
+
+ minimatch@3.1.5:
+ dependencies:
+ brace-expansion: 1.1.18
+
mlly@1.8.2:
dependencies:
- acorn: 8.16.0
+ acorn: 8.18.0
pathe: 2.0.3
pkg-types: 1.3.1
- ufo: 1.6.3
+ ufo: 1.6.4
ms@2.1.3: {}
- msw@2.13.4(@types/node@20.19.39)(typescript@5.9.3):
+ msw@2.15.0(@types/node@20.19.43)(typescript@5.9.3):
dependencies:
- '@inquirer/confirm': 6.0.12(@types/node@20.19.39)
- '@mswjs/interceptors': 0.41.4
+ '@inquirer/confirm': 6.2.0(@types/node@20.19.43)
+ '@mswjs/interceptors': 0.41.9
'@open-draft/deferred-promise': 3.0.0
'@types/statuses': 2.0.6
cookie: 1.1.1
- graphql: 16.13.2
+ graphql: 16.14.2
headers-polyfill: 5.0.1
is-node-process: 1.2.0
outvariant: 1.4.3
path-to-regexp: 6.3.0
picocolors: 1.1.1
- rettime: 0.11.8
+ rettime: 0.11.11
statuses: 2.0.2
strict-event-emitter: 0.5.1
- tough-cookie: 6.0.1
- type-fest: 5.6.0
+ tough-cookie: 6.0.2
+ type-fest: 5.8.0
until-async: 3.0.2
- yargs: 17.7.2
+ yargs: 17.7.3
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
@@ -1745,12 +2028,18 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
- nanoid@3.3.12: {}
+ nanoid@3.3.18: {}
object-assign@4.1.1: {}
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
outvariant@1.4.3: {}
+ path-is-absolute@1.0.1: {}
+
path-to-regexp@6.3.0: {}
pathe@2.0.3: {}
@@ -1759,7 +2048,7 @@ snapshots:
picocolors@1.1.1: {}
- picomatch@4.0.4: {}
+ picomatch@4.0.5: {}
pirates@4.0.7: {}
@@ -1769,16 +2058,15 @@ snapshots:
mlly: 1.8.2
pathe: 2.0.3
- postcss-load-config@6.0.1(postcss@8.5.15)(yaml@2.4.5):
+ postcss-load-config@6.0.1(postcss@8.5.26):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
- postcss: 8.5.15
- yaml: 2.4.5
+ postcss: 8.5.26
- postcss@8.5.15:
+ postcss@8.5.26:
dependencies:
- nanoid: 3.3.12
+ nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -1788,73 +2076,45 @@ snapshots:
resolve-from@5.0.0: {}
- rettime@0.11.8: {}
+ rettime@0.11.11: {}
reusify@1.1.0: {}
- rollup@4.60.2:
- dependencies:
- '@types/estree': 1.0.8
- optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.60.2
- '@rollup/rollup-android-arm64': 4.60.2
- '@rollup/rollup-darwin-arm64': 4.60.2
- '@rollup/rollup-darwin-x64': 4.60.2
- '@rollup/rollup-freebsd-arm64': 4.60.2
- '@rollup/rollup-freebsd-x64': 4.60.2
- '@rollup/rollup-linux-arm-gnueabihf': 4.60.2
- '@rollup/rollup-linux-arm-musleabihf': 4.60.2
- '@rollup/rollup-linux-arm64-gnu': 4.60.2
- '@rollup/rollup-linux-arm64-musl': 4.60.2
- '@rollup/rollup-linux-loong64-gnu': 4.60.2
- '@rollup/rollup-linux-loong64-musl': 4.60.2
- '@rollup/rollup-linux-ppc64-gnu': 4.60.2
- '@rollup/rollup-linux-ppc64-musl': 4.60.2
- '@rollup/rollup-linux-riscv64-gnu': 4.60.2
- '@rollup/rollup-linux-riscv64-musl': 4.60.2
- '@rollup/rollup-linux-s390x-gnu': 4.60.2
- '@rollup/rollup-linux-x64-gnu': 4.60.2
- '@rollup/rollup-linux-x64-musl': 4.60.2
- '@rollup/rollup-openbsd-x64': 4.60.2
- '@rollup/rollup-openharmony-arm64': 4.60.2
- '@rollup/rollup-win32-arm64-msvc': 4.60.2
- '@rollup/rollup-win32-ia32-msvc': 4.60.2
- '@rollup/rollup-win32-x64-gnu': 4.60.2
- '@rollup/rollup-win32-x64-msvc': 4.60.2
- fsevents: 2.3.3
-
- rollup@4.61.1:
+ rollup@4.62.4:
dependencies:
'@types/estree': 1.0.9
optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.61.1
- '@rollup/rollup-android-arm64': 4.61.1
- '@rollup/rollup-darwin-arm64': 4.61.1
- '@rollup/rollup-darwin-x64': 4.61.1
- '@rollup/rollup-freebsd-arm64': 4.61.1
- '@rollup/rollup-freebsd-x64': 4.61.1
- '@rollup/rollup-linux-arm-gnueabihf': 4.61.1
- '@rollup/rollup-linux-arm-musleabihf': 4.61.1
- '@rollup/rollup-linux-arm64-gnu': 4.61.1
- '@rollup/rollup-linux-arm64-musl': 4.61.1
- '@rollup/rollup-linux-loong64-gnu': 4.61.1
- '@rollup/rollup-linux-loong64-musl': 4.61.1
- '@rollup/rollup-linux-ppc64-gnu': 4.61.1
- '@rollup/rollup-linux-ppc64-musl': 4.61.1
- '@rollup/rollup-linux-riscv64-gnu': 4.61.1
- '@rollup/rollup-linux-riscv64-musl': 4.61.1
- '@rollup/rollup-linux-s390x-gnu': 4.61.1
- '@rollup/rollup-linux-x64-gnu': 4.61.1
- '@rollup/rollup-linux-x64-musl': 4.61.1
- '@rollup/rollup-openbsd-x64': 4.61.1
- '@rollup/rollup-openharmony-arm64': 4.61.1
- '@rollup/rollup-win32-arm64-msvc': 4.61.1
- '@rollup/rollup-win32-ia32-msvc': 4.61.1
- '@rollup/rollup-win32-x64-gnu': 4.61.1
- '@rollup/rollup-win32-x64-msvc': 4.61.1
+ '@napi-rs/lzma-linux-x64-gnu': 1.5.1
+ '@rollup/rollup-android-arm-eabi': 4.62.4
+ '@rollup/rollup-android-arm64': 4.62.4
+ '@rollup/rollup-darwin-arm64': 4.62.4
+ '@rollup/rollup-darwin-x64': 4.62.4
+ '@rollup/rollup-freebsd-arm64': 4.62.4
+ '@rollup/rollup-freebsd-x64': 4.62.4
+ '@rollup/rollup-linux-arm-gnueabihf': 4.62.4
+ '@rollup/rollup-linux-arm-musleabihf': 4.62.4
+ '@rollup/rollup-linux-arm64-gnu': 4.62.4
+ '@rollup/rollup-linux-arm64-musl': 4.62.4
+ '@rollup/rollup-linux-loong64-gnu': 4.62.4
+ '@rollup/rollup-linux-loong64-musl': 4.62.4
+ '@rollup/rollup-linux-ppc64-gnu': 4.62.4
+ '@rollup/rollup-linux-ppc64-musl': 4.62.4
+ '@rollup/rollup-linux-riscv64-gnu': 4.62.4
+ '@rollup/rollup-linux-riscv64-musl': 4.62.4
+ '@rollup/rollup-linux-s390x-gnu': 4.62.4
+ '@rollup/rollup-linux-x64-gnu': 4.62.4
+ '@rollup/rollup-linux-x64-musl': 4.62.4
+ '@rollup/rollup-openbsd-x64': 4.62.4
+ '@rollup/rollup-openharmony-arm64': 4.62.4
+ '@rollup/rollup-win32-arm64-msvc': 4.62.4
+ '@rollup/rollup-win32-ia32-msvc': 4.62.4
+ '@rollup/rollup-win32-x64-gnu': 4.62.4
+ '@rollup/rollup-win32-x64-msvc': 4.62.4
fsevents: 2.3.3
- set-cookie-parser@3.1.0: {}
+ semver@7.8.5: {}
+
+ set-cookie-parser@3.1.2: {}
siginfo@2.0.0: {}
@@ -1882,6 +2142,10 @@ snapshots:
dependencies:
ansi-regex: 5.0.1
+ strip-literal@2.1.1:
+ dependencies:
+ js-tokens: 9.0.1
+
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
@@ -1893,11 +2157,21 @@ snapshots:
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.7
- tinyglobby: 0.2.16
+ tinyglobby: 0.2.17
ts-interface-checker: 0.1.13
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
tagged-tag@1.0.0: {}
+ test-exclude@6.0.0:
+ dependencies:
+ '@istanbuljs/schema': 0.1.6
+ glob: 7.2.3
+ minimatch: 3.1.5
+
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@@ -1910,15 +2184,10 @@ snapshots:
tinyexec@0.3.2: {}
- tinyglobby@0.2.16:
- dependencies:
- fdir: 6.5.0(picomatch@4.0.4)
- picomatch: 4.0.4
-
tinyglobby@0.2.17:
dependencies:
- fdir: 6.5.0(picomatch@4.0.4)
- picomatch: 4.0.4
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
tinypool@1.1.1: {}
@@ -1926,21 +2195,21 @@ snapshots:
tinyspy@4.0.4: {}
- tldts-core@7.0.28: {}
+ tldts-core@7.4.10: {}
- tldts@7.0.28:
+ tldts@7.4.10:
dependencies:
- tldts-core: 7.0.28
+ tldts-core: 7.4.10
- tough-cookie@6.0.1:
+ tough-cookie@6.0.2:
dependencies:
- tldts: 7.0.28
+ tldts: 7.4.10
tree-kill@1.2.2: {}
ts-interface-checker@0.1.13: {}
- tsup@8.5.1(postcss@8.5.15)(typescript@5.9.3)(yaml@2.4.5):
+ tsup@8.5.1(postcss@8.5.26)(typescript@5.9.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.7)
cac: 6.7.14
@@ -1951,16 +2220,16 @@ snapshots:
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
- postcss-load-config: 6.0.1(postcss@8.5.15)(yaml@2.4.5)
+ postcss-load-config: 6.0.1(postcss@8.5.26)
resolve-from: 5.0.0
- rollup: 4.60.2
+ rollup: 4.62.4
source-map: 0.7.6
sucrase: 3.35.1
tinyexec: 0.3.2
- tinyglobby: 0.2.16
+ tinyglobby: 0.2.17
tree-kill: 1.2.2
optionalDependencies:
- postcss: 8.5.15
+ postcss: 8.5.26
typescript: 5.9.3
transitivePeerDependencies:
- jiti
@@ -1968,25 +2237,25 @@ snapshots:
- tsx
- yaml
- type-fest@5.6.0:
+ type-fest@5.8.0:
dependencies:
tagged-tag: 1.0.0
typescript@5.9.3: {}
- ufo@1.6.3: {}
+ ufo@1.6.4: {}
undici-types@6.21.0: {}
until-async@3.0.2: {}
- vite-node@3.2.4(@types/node@20.19.39)(yaml@2.4.5):
+ vite-node@3.2.4(@types/node@20.19.43):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
- vite: 7.3.5(@types/node@20.19.39)(yaml@2.4.5)
+ vite: 7.3.6(@types/node@20.19.43)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -2001,46 +2270,45 @@ snapshots:
- tsx
- yaml
- vite@7.3.5(@types/node@20.19.39)(yaml@2.4.5):
+ vite@7.3.6(@types/node@20.19.43):
dependencies:
- esbuild: 0.27.7
- fdir: 6.5.0(picomatch@4.0.4)
- picomatch: 4.0.4
- postcss: 8.5.15
- rollup: 4.61.1
+ esbuild: 0.28.2
+ fdir: 6.5.0(picomatch@4.0.5)
+ picomatch: 4.0.5
+ postcss: 8.5.26
+ rollup: 4.62.4
tinyglobby: 0.2.17
optionalDependencies:
- '@types/node': 20.19.39
+ '@types/node': 20.19.43
fsevents: 2.3.3
- yaml: 2.4.5
- vitest@3.2.6(@types/node@20.19.39)(msw@2.13.4(@types/node@20.19.39)(typescript@5.9.3))(yaml@2.4.5):
+ vitest@3.2.7(@types/node@20.19.43)(msw@2.15.0(@types/node@20.19.43)(typescript@5.9.3)):
dependencies:
'@types/chai': 5.2.3
- '@vitest/expect': 3.2.6
- '@vitest/mocker': 3.2.6(msw@2.13.4(@types/node@20.19.39)(typescript@5.9.3))(vite@7.3.5(@types/node@20.19.39)(yaml@2.4.5))
- '@vitest/pretty-format': 3.2.6
- '@vitest/runner': 3.2.6
- '@vitest/snapshot': 3.2.6
- '@vitest/spy': 3.2.6
- '@vitest/utils': 3.2.6
+ '@vitest/expect': 3.2.7
+ '@vitest/mocker': 3.2.7(msw@2.15.0(@types/node@20.19.43)(typescript@5.9.3))(vite@7.3.6(@types/node@20.19.43))
+ '@vitest/pretty-format': 3.2.7
+ '@vitest/runner': 3.2.7
+ '@vitest/snapshot': 3.2.7
+ '@vitest/spy': 3.2.7
+ '@vitest/utils': 3.2.7
chai: 5.3.3
debug: 4.4.3
- expect-type: 1.3.0
+ expect-type: 1.4.0
magic-string: 0.30.21
pathe: 2.0.3
- picomatch: 4.0.4
+ picomatch: 4.0.5
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.17
tinypool: 1.1.1
tinyrainbow: 2.0.0
- vite: 7.3.5(@types/node@20.19.39)(yaml@2.4.5)
- vite-node: 3.2.4(@types/node@20.19.39)(yaml@2.4.5)
+ vite: 7.3.6(@types/node@20.19.43)
+ vite-node: 3.2.4(@types/node@20.19.43)
why-is-node-running: 2.3.0
optionalDependencies:
- '@types/node': 20.19.39
+ '@types/node': 20.19.43
transitivePeerDependencies:
- jiti
- less
@@ -2066,14 +2334,13 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
- y18n@5.0.8: {}
+ wrappy@1.0.2: {}
- yaml@2.4.5:
- optional: true
+ y18n@5.0.8: {}
yargs-parser@21.1.1: {}
- yargs@17.7.2:
+ yargs@17.7.3:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
diff --git a/src/batch/client.ts b/src/batch/client.ts
new file mode 100644
index 0000000..c0c2549
--- /dev/null
+++ b/src/batch/client.ts
@@ -0,0 +1,671 @@
+import {
+ type DownloadToDirOptions,
+ type DownloadToMemoryOptions,
+ type DownloadedResult,
+ downloadTaskToFile,
+ downloadTaskToMemory,
+ downloadToDir,
+ downloadToMemory,
+} from "./download.js";
+import { ZenRowsBatchError } from "./errors.js";
+import { type CostEstimate, type ParamMap, type TaskLike, estimateCost } from "./estimate.js";
+import {
+ CurrentRun,
+ ExportHandle,
+ ExportRef,
+ JobHandle,
+ JobRef,
+ RunHandle,
+ RunRef,
+} from "./resources.js";
+import type { Schedule } from "./schedule.js";
+import { BatchTransport } from "./transport.js";
+import type {
+ AddTasksResponse,
+ CreateJobInputResponse,
+ Export,
+ HMACKeyCreated,
+ HMACKeyFinalized,
+ HMACKeyList,
+ Job,
+ JobSchedule,
+ JobStatus,
+ JobType,
+ ListJobRunsResponse,
+ ListJobsResponse,
+ ListResultsResponse,
+ RerunJobResponse,
+ Run,
+ StartExportResponse,
+ TaskHistoryResponse,
+ TaskResult,
+ TestWebhookResponse,
+ WebhookConfig,
+} from "./types.js";
+import { pollUntil } from "./waiters.js";
+
+const DEFAULT_BATCH_API_URL = "https://async.api.zenrows.com/v1";
+
+const TERMINAL_RUN_STATUSES = new Set(["completed", "stopped", "deleted"]);
+const TERMINAL_EXPORT_STATUSES = new Set(["completed", "failed"]);
+
+export interface BatchClientConfig {
+ /** Override the Batch API base URL. Matches the Go/Python batch clients' base-URL override. */
+ baseURL?: string;
+ /** Retries for transient failures (429/502/503/504 + network errors) on idempotent requests. Default 3. */
+ retries?: number;
+}
+
+export interface BatchTask {
+ url: string;
+ external_id?: string;
+ metadata?: Record;
+ zenrows_params?: Record;
+}
+
+export interface BatchWebhookConfig {
+ url: string;
+ signature?: boolean;
+}
+
+export interface SubmitJobOptions {
+ type?: JobType;
+ status?: JobStatus;
+ zenrows_params?: Record;
+ tasks?: BatchTask[];
+ file_input_id?: string;
+ external_id?: string;
+ name?: string;
+ metadata?: Record;
+ webhook?: BatchWebhookConfig;
+ idempotencyKey?: string;
+ /** Block until ingestion finishes for a large (202) submission — see `JobRef.waitForIngest`. */
+ waitForIngest?: boolean;
+}
+
+export interface SubmitTypedOptions {
+ zenrowsParams?: ParamMap;
+ externalId?: string;
+ name?: string;
+ metadata?: Record;
+ webhook?: BatchWebhookConfig;
+ idempotencyKey?: string;
+ waitForIngest?: boolean;
+}
+
+export interface ListJobsOptions {
+ status?: JobStatus;
+ type?: JobType;
+ limit?: number;
+ cursor?: string;
+}
+
+export interface GetResultsOptions {
+ runId?: string;
+ cursor?: string;
+ limit?: number;
+ status?: string;
+}
+
+export interface WaitForRunOptions {
+ runId?: string;
+ targetStatuses?: Set;
+ failureStatuses?: Set;
+ timeout?: number;
+ pollInterval?: number;
+ maxPollInterval?: number;
+}
+
+type TaskInput = string | { url: string; external_id?: string; metadata?: Record };
+
+function coerceUrl(item: TaskInput): {
+ url: string;
+ external_id?: string;
+ metadata?: Record;
+} {
+ return typeof item === "string" ? { url: item } : item;
+}
+
+/**
+ * Client for the Zenrows Batch API (async, job/run/task model). Usable standalone
+ * (`new ZenRowsBatchClient(apiKey)`, matching the Go and Python SDKs' batch clients) or via
+ * the `client.batch` convenience property on a `ZenRows` instance.
+ *
+ * The **main pattern** is resource-style: `job(id)` / `submit*` return a `JobRef` (id-only,
+ * no network); `getJob()` / `iterJobs()` return a loaded `JobHandle` with `.data`. Mutating
+ * operations (`close`, `run.stop`, …) return a FRESH handle carrying the server's updated
+ * state rather than mutating in place.
+ */
+export class ZenRowsBatchClient {
+ readonly apiKey: string;
+ private readonly transport: BatchTransport;
+
+ constructor(apiKey: string, config: BatchClientConfig = {}) {
+ this.apiKey = apiKey;
+ this.transport = new BatchTransport(
+ config.baseURL ?? DEFAULT_BATCH_API_URL,
+ apiKey,
+ config.retries,
+ );
+ }
+
+ // ===== submit =====
+
+ async submitJob(options: SubmitJobOptions = {}): Promise {
+ const { idempotencyKey, waitForIngest, ...body } = options;
+ const resp = await this.transport.requestJson<{
+ job_id: string;
+ status: JobStatus;
+ latest_run?: Run;
+ accepted_tasks: number;
+ webhook?: WebhookConfig;
+ }>("POST", "/jobs", { body, idempotencyKey });
+ const ref = new JobRef(this, resp.job_id, resp);
+ if (waitForIngest && resp.latest_run && resp.latest_run.ingest_status === "pending") {
+ return ref.waitForIngest();
+ }
+ return ref;
+ }
+
+ /** Submit a one-shot scraping job (closed, all tasks known upfront). */
+ async submitRegular(
+ urls?: TaskInput[],
+ fileInputId?: string,
+ opts: SubmitTypedOptions = {},
+ ): Promise {
+ if (urls !== undefined && fileInputId !== undefined) {
+ throw new Error("submitRegular: pass urls OR fileInputId, not both.");
+ }
+ if (urls === undefined && fileInputId === undefined) {
+ throw new Error(
+ "submitRegular: closed jobs require urls or fileInputId (use submitOpen for the open/extend pattern).",
+ );
+ }
+ return this.submitJob({
+ type: "regular",
+ status: "closed",
+ tasks: urls?.map(coerceUrl),
+ file_input_id: fileInputId,
+ zenrows_params: opts.zenrowsParams,
+ external_id: opts.externalId,
+ name: opts.name,
+ metadata: opts.metadata,
+ webhook: opts.webhook,
+ idempotencyKey: opts.idempotencyKey,
+ waitForIngest: opts.waitForIngest,
+ });
+ }
+
+ /** Submit a streaming-style job that stays open for more tasks via `JobRef.addTasks`. */
+ submitOpen(urls?: TaskInput[], opts: SubmitTypedOptions = {}): Promise {
+ return this.submitJob({
+ type: "regular",
+ status: "open",
+ tasks: urls?.map(coerceUrl),
+ zenrows_params: opts.zenrowsParams,
+ external_id: opts.externalId,
+ name: opts.name,
+ metadata: opts.metadata,
+ webhook: opts.webhook,
+ idempotencyKey: opts.idempotencyKey,
+ waitForIngest: opts.waitForIngest,
+ });
+ }
+
+ /** Submit a scheduled job. `schedule` is one of the `At`/`Rate`/`Calendar` builders. */
+ async submitScheduled(
+ schedule: Schedule,
+ urls?: TaskInput[],
+ fileInputId?: string,
+ opts: SubmitTypedOptions = {},
+ ): Promise {
+ if (urls !== undefined && fileInputId !== undefined) {
+ throw new Error("submitScheduled: pass urls OR fileInputId, not both.");
+ }
+ return this.submitJob({
+ type: "scheduled",
+ status: "closed",
+ tasks: urls?.map(coerceUrl),
+ file_input_id: fileInputId,
+ zenrows_params: opts.zenrowsParams,
+ external_id: opts.externalId,
+ name: opts.name,
+ metadata: opts.metadata,
+ webhook: opts.webhook,
+ idempotencyKey: opts.idempotencyKey,
+ // biome-ignore lint/suspicious/noExplicitAny: schedule builder output matches JobSchedule at the wire boundary
+ ...{ schedule: schedule.toRequestBody() as unknown as any },
+ });
+ }
+
+ // ===== cost estimation (local, no API call) =====
+
+ estimateCost(tasks: Iterable, zenrowsParams?: ParamMap): CostEstimate {
+ return estimateCost(tasks, zenrowsParams);
+ }
+
+ // ===== jobs =====
+
+ /** A `JobRef` for an existing job with no network call — prefer this to act on a known id. */
+ job(jobId: string): JobRef {
+ return new JobRef(this, jobId);
+ }
+
+ async getJob(jobId: string): Promise {
+ return new JobHandle(this, jobId, await this._getJobData(jobId));
+ }
+
+ async listJobs(options: ListJobsOptions = {}): Promise {
+ return this.transport.requestJson("GET", "/jobs", { query: options });
+ }
+
+ async *iterJobs(options: Omit = {}): AsyncGenerator {
+ let cursor: string | undefined;
+ while (true) {
+ const page = await this.listJobs({ ...options, cursor });
+ for (const job of page.jobs) {
+ yield new JobHandle(this, job.job_id, job);
+ }
+ cursor = page.next_cursor;
+ if (!cursor) return;
+ }
+ }
+
+ // ===== runs =====
+
+ /** A `RunRef` for an existing run with no network call. */
+ run(jobId: string, runId: string): RunRef {
+ return new RunRef(this, jobId, runId);
+ }
+
+ async getRun(jobId: string, runId: string): Promise {
+ return new RunHandle(this, jobId, runId, await this._getRunData(jobId, runId));
+ }
+
+ async listRuns(
+ jobId: string,
+ options: { limit?: number; cursor?: string } = {},
+ ): Promise {
+ return this.transport.requestJson("GET", `/jobs/${jobId}/runs`, { query: options });
+ }
+
+ async *iterRuns(jobId: string, pageSize?: number): AsyncGenerator {
+ let cursor: string | undefined;
+ while (true) {
+ const page = await this.listRuns(jobId, { limit: pageSize, cursor });
+ for (const run of page.runs) {
+ yield new RunHandle(this, jobId, run.run_id, run);
+ }
+ cursor = page.next_cursor;
+ if (!cursor) return;
+ }
+ }
+
+ // ===== results / content =====
+
+ async getResults(jobId: string, options: GetResultsOptions = {}): Promise {
+ const { runId, ...query } = options;
+ const path = runId ? `/jobs/${jobId}/runs/${runId}/results` : `/jobs/${jobId}/results`;
+ return this.transport.requestJson("GET", path, { query });
+ }
+
+ async *iterResults(
+ jobId: string,
+ options: { runId?: string; status?: string } = {},
+ ): AsyncGenerator {
+ let cursor: string | undefined;
+ while (true) {
+ const page = await this.getResults(jobId, { ...options, cursor });
+ yield* page.results;
+ cursor = page.next_cursor;
+ if (!cursor) return;
+ }
+ }
+
+ /** Returns the scraped page's raw content as-is (HTML/text, not JSON). */
+ async getTaskContent(
+ jobId: string,
+ taskId: string,
+ options: { runId?: string } = {},
+ ): Promise {
+ const path = options.runId
+ ? `/jobs/${jobId}/runs/${options.runId}/tasks/${taskId}/content`
+ : `/jobs/${jobId}/tasks/${taskId}/content`;
+ const response = await this.transport.requestRaw("GET", path);
+ return response.text();
+ }
+
+ getTaskHistory(
+ jobId: string,
+ taskId: string,
+ options: { runId?: string } = {},
+ ): Promise {
+ const path = options.runId
+ ? `/jobs/${jobId}/runs/${options.runId}/tasks/${taskId}/history`
+ : `/jobs/${jobId}/tasks/${taskId}/history`;
+ return this.transport.requestJson("GET", path);
+ }
+
+ // ===== downloads =====
+
+ downloadToDir(
+ jobId: string,
+ runId: string | undefined,
+ targetDir: string,
+ opts: DownloadToDirOptions = {},
+ ) {
+ return downloadToDir(
+ this.iterResults(jobId, { runId, status: opts.status ?? "successful" }),
+ targetDir,
+ opts,
+ );
+ }
+
+ downloadToMemory(
+ jobId: string,
+ runId: string | undefined,
+ opts: DownloadToMemoryOptions & { status?: string } = {},
+ ) {
+ return downloadToMemory(
+ this.iterResults(jobId, { runId, status: opts.status ?? "successful" }),
+ opts,
+ );
+ }
+
+ downloadTaskToFile(task: TaskResult, target: string): Promise {
+ return downloadTaskToFile(task, target);
+ }
+
+ downloadTaskToMemory(task: TaskResult): Promise {
+ return downloadTaskToMemory(task);
+ }
+
+ // ===== webhooks =====
+
+ getJobWebhook(jobId: string): Promise {
+ return this.transport.requestJson("GET", `/jobs/${jobId}/webhook`);
+ }
+
+ putJobWebhook(jobId: string, config: BatchWebhookConfig): Promise {
+ return this.transport.requestJson("PUT", `/jobs/${jobId}/webhook`, {
+ body: { signature: false, ...config },
+ });
+ }
+
+ async deleteJobWebhook(jobId: string): Promise {
+ await this.transport.requestJson("DELETE", `/jobs/${jobId}/webhook`);
+ }
+
+ testWebhook(config: BatchWebhookConfig): Promise {
+ return this.transport.requestJson("POST", "/webhook/test", {
+ body: { signature: false, ...config },
+ });
+ }
+
+ // ===== HMAC key lifecycle =====
+
+ listHmacKeys(): Promise {
+ return this.transport.requestJson("GET", "/hmac/keys");
+ }
+
+ /** Capture the returned `secret` HERE — it is not revealed again. */
+ rotateHmacKey(): Promise {
+ return this.transport.requestJson("POST", "/hmac/keys/rotate");
+ }
+
+ finalizeHmacKey(): Promise {
+ return this.transport.requestJson("POST", "/hmac/keys/rotate/finalize");
+ }
+
+ async cancelHmacRotation(): Promise {
+ await this.transport.requestJson("DELETE", "/hmac/keys/rotate");
+ }
+
+ // ===== file inputs (CSV uploads) =====
+
+ createJobInput(body: {
+ type: "csv";
+ csv: {
+ delimiter?: string;
+ quote?: string;
+ header?: boolean;
+ fields: { url: string | number; external_id?: string | number };
+ };
+ }): Promise {
+ return this.transport.requestJson("POST", "/job_inputs", { body });
+ }
+
+ /** Allocate a CSV slot + PUT the body. Returns the `file_input_id` to pass to `submitJob`. */
+ async uploadCsv(
+ data: Uint8Array | string,
+ options: {
+ fields: { url: string | number; external_id?: string | number };
+ header?: boolean;
+ delimiter?: string;
+ quote?: string;
+ },
+ ): Promise {
+ const created = await this.createJobInput({
+ type: "csv",
+ csv: {
+ delimiter: options.delimiter ?? ",",
+ quote: options.quote ?? '"',
+ header: options.header ?? false,
+ fields: options.fields,
+ },
+ });
+ const headers = { "Content-Type": "text/csv", ...(created.upload.headers ?? {}) };
+ // The presigned URL lives on a different host (S3) — a bare fetch so our API key never
+ // reaches it, matching the Python SDK's explicit design choice.
+ const response = await fetch(created.upload.url, {
+ method: created.upload.method,
+ headers,
+ body: data,
+ });
+ if (!response.ok) {
+ throw new ZenRowsBatchError(response.status, undefined);
+ }
+ return created.file_input_id;
+ }
+
+ // ===== results exports =====
+
+ async startResultsExport(jobId: string, runId: string): Promise {
+ const resp = await this._postExportStart(jobId, runId);
+ return new ExportRef(this, jobId, runId, resp.export_id, resp);
+ }
+
+ async getResultsExport(jobId: string, runId: string, exportId: string): Promise {
+ return new ExportHandle(
+ this,
+ jobId,
+ runId,
+ exportId,
+ await this._getExport(jobId, runId, exportId),
+ );
+ }
+
+ async waitForExport(
+ jobId: string,
+ runId: string,
+ exportId: string,
+ options: {
+ targetStatuses?: Set;
+ timeout?: number;
+ pollInterval?: number;
+ maxPollInterval?: number;
+ } = {},
+ ): Promise {
+ const target = options.targetStatuses ?? TERMINAL_EXPORT_STATUSES;
+ return pollUntil(() => this._getExport(jobId, runId, exportId), {
+ isDone: (e) => target.has(e.status),
+ timeout: options.timeout ?? 600,
+ initialInterval: options.pollInterval ?? 2,
+ maxInterval: options.maxPollInterval ?? 15,
+ });
+ }
+
+ /** Start an export, wait for it, and save the zip to `targetPath`. Capped at 1 GiB per run. */
+ async downloadAllResults(
+ jobId: string,
+ runId: string,
+ targetPath: string,
+ options: { waitTimeout?: number; pollInterval?: number } = {},
+ ): Promise {
+ const exportRef = await this.startResultsExport(jobId, runId);
+ const final = await this.waitForExport(jobId, runId, exportRef.exportId, {
+ timeout: options.waitTimeout ?? 600,
+ pollInterval: options.pollInterval ?? 2,
+ });
+ if (final.status !== "completed" || !final.download_url) {
+ throw new Error(final.error ?? "export completed but server returned no download_url");
+ }
+ const { mkdir } = await import("node:fs/promises");
+ const { dirname } = await import("node:path");
+ const { createWriteStream } = await import("node:fs");
+ const { Readable } = await import("node:stream");
+ const { pipeline } = await import("node:stream/promises");
+ await mkdir(dirname(targetPath), { recursive: true });
+ const response = await fetch(final.download_url);
+ if (!response.ok) {
+ throw new ZenRowsBatchError(response.status, undefined);
+ }
+ await pipeline(Readable.fromWeb(response.body as never), createWriteStream(targetPath));
+ return targetPath;
+ }
+
+ // ===== waiter =====
+
+ async waitForRun(jobId: string, options: WaitForRunOptions = {}): Promise {
+ return this._waitForRunRaw(jobId, options);
+ }
+
+ // ==========================================================
+ // "raw" methods — no resource wrapping; used by handles internally.
+ // ==========================================================
+
+ /** @internal */
+ async _getJobData(jobId: string): Promise {
+ return this.transport.requestJson("GET", `/jobs/${jobId}`);
+ }
+
+ /** @internal */
+ async _getRunData(jobId: string, runId: string): Promise {
+ return this.transport.requestJson("GET", `/jobs/${jobId}/runs/${runId}`);
+ }
+
+ /** @internal */
+ async _postClose(jobId: string): Promise {
+ return this.transport.requestJson("POST", `/jobs/${jobId}/close`);
+ }
+
+ /** @internal */
+ async _postStop(jobId: string): Promise {
+ return this.transport.requestJson("POST", `/jobs/${jobId}/stop`);
+ }
+
+ /** @internal */
+ async _postPause(jobId: string): Promise {
+ return this.transport.requestJson("POST", `/jobs/${jobId}/pause`);
+ }
+
+ /** @internal */
+ async _postResume(jobId: string): Promise {
+ return this.transport.requestJson("POST", `/jobs/${jobId}/resume`);
+ }
+
+ /** @internal */
+ async _delete(jobId: string): Promise {
+ await this.transport.requestJson("DELETE", `/jobs/${jobId}`);
+ }
+
+ /** @internal */
+ async _deleteRun(jobId: string, runId: string): Promise {
+ await this.transport.requestJson("DELETE", `/jobs/${jobId}/runs/${runId}`);
+ }
+
+ /** @internal */
+ async _postRerun(
+ jobId: string,
+ options: { status?: string | string[]; idempotencyKey?: string } = {},
+ ): Promise {
+ const status = Array.isArray(options.status) ? options.status.join(",") : options.status;
+ return this.transport.requestJson("POST", `/jobs/${jobId}/rerun`, {
+ query: status ? { status } : undefined,
+ idempotencyKey: options.idempotencyKey,
+ });
+ }
+
+ /** @internal */
+ async _putSchedule(jobId: string, schedule: JobSchedule): Promise {
+ return this.transport.requestJson("PUT", `/jobs/${jobId}/schedule`, { body: schedule });
+ }
+
+ /** @internal */
+ async _postScheduleState(jobId: string, state: "paused" | "active"): Promise {
+ return this.transport.requestJson("POST", `/jobs/${jobId}/schedule/state`, {
+ body: { schedule_state: state },
+ });
+ }
+
+ /** @internal */
+ async _postTasks(
+ jobId: string,
+ tasks: BatchTask[],
+ options: { lastBatch?: boolean } = {},
+ ): Promise {
+ return this.transport.requestJson("POST", `/jobs/${jobId}/tasks`, {
+ body: { tasks, last_batch: options.lastBatch ?? false },
+ });
+ }
+
+ /** @internal */
+ async _postExportStart(jobId: string, runId: string): Promise {
+ return this.transport.requestJson("POST", `/jobs/${jobId}/runs/${runId}/exports`);
+ }
+
+ /** @internal */
+ async _getExport(jobId: string, runId: string, exportId: string): Promise {
+ return this.transport.requestJson("GET", `/jobs/${jobId}/runs/${runId}/exports/${exportId}`);
+ }
+
+ /** @internal */
+ async _waitForRunRaw(jobId: string, options: WaitForRunOptions): Promise {
+ const target = options.targetStatuses ?? TERMINAL_RUN_STATUSES;
+ const fetchRun = async (): Promise => {
+ if (!options.runId) {
+ const job = await this._getJobData(jobId);
+ return job.latest_run;
+ }
+ return this._getRunData(jobId, options.runId);
+ };
+ return pollUntil(fetchRun, {
+ isDone: (run) => run !== undefined && target.has(run.status),
+ isFailure: (run) => run !== undefined && Boolean(options.failureStatuses?.has(run.status)),
+ timeout: options.timeout ?? 300,
+ initialInterval: options.pollInterval ?? 2,
+ maxInterval: options.maxPollInterval ?? 15,
+ // biome-ignore lint/suspicious/noExplicitAny: fetchRun can resolve undefined for a not-yet-fired scheduled job
+ }) as Promise;
+ }
+
+ /** @internal */
+ async _waitForIngestRaw(
+ jobId: string,
+ options: { timeout?: number; pollInterval?: number; maxPollInterval?: number } = {},
+ ): Promise {
+ return pollUntil(() => this._getJobData(jobId), {
+ isDone: (job) => {
+ const run = job.latest_run;
+ if (!run || TERMINAL_RUN_STATUSES.has(run.status)) return true;
+ return run.ingest_status !== "pending";
+ },
+ timeout: options.timeout ?? 300,
+ initialInterval: options.pollInterval ?? 2,
+ maxInterval: options.maxPollInterval ?? 15,
+ });
+ }
+}
+
+export { CurrentRun, ExportHandle, ExportRef, JobHandle, JobRef, RunHandle, RunRef };
+export type { DownloadedResult };
diff --git a/src/batch/download.ts b/src/batch/download.ts
new file mode 100644
index 0000000..73ef72c
--- /dev/null
+++ b/src/batch/download.ts
@@ -0,0 +1,159 @@
+import { createWriteStream } from "node:fs";
+import { mkdir } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { Readable } from "node:stream";
+import { pipeline } from "node:stream/promises";
+import type { TaskResult } from "./types.js";
+
+export const DEFAULT_MAX_FILES = 100_000;
+export const DEFAULT_MAX_BYTES_PER_FILE = 50 * 1024 * 1024;
+export const DEFAULT_MAX_COUNT_IN_MEMORY = 10_000;
+export const DEFAULT_MAX_TOTAL_BYTES_IN_MEMORY = 500 * 1024 * 1024;
+
+export interface DownloadedResult {
+ taskId: string;
+ externalId?: string;
+ url: string;
+ body: Buffer;
+}
+
+/**
+ * Every result body lives at a presigned `result_url` — no auth header, no API content
+ * endpoint in the loop, matching the Python SDK's approach exactly.
+ */
+async function fetchResultBody(task: TaskResult, maxBytes?: number): Promise {
+ if (!task.result_url) {
+ throw new Error(`task ${task.task_id} has no result_url (not a successful task?)`);
+ }
+ const response = await fetch(task.result_url);
+ if (!response.ok) {
+ throw new Error(`downloading task ${task.task_id} failed with status ${response.status}`);
+ }
+ const buffer = Buffer.from(await response.arrayBuffer());
+ if (maxBytes !== undefined && buffer.byteLength > maxBytes) {
+ throw new Error(
+ `task ${task.task_id}'s body (${buffer.byteLength} bytes) exceeds maxBytesPerFile (${maxBytes})`,
+ );
+ }
+ return buffer;
+}
+
+/** Run `worker` over `items` with at most `concurrency` in flight at once. */
+async function runPool(
+ items: T[],
+ concurrency: number,
+ worker: (item: T, index: number) => Promise,
+): Promise {
+ let cursor = 0;
+ const errors: unknown[] = [];
+ async function next(): Promise {
+ while (cursor < items.length) {
+ const index = cursor++;
+ try {
+ await worker(items[index] as T, index);
+ } catch (error) {
+ errors.push(error);
+ }
+ }
+ }
+ const workers = Array.from({ length: Math.max(1, concurrency) }, () => next());
+ await Promise.all(workers);
+ if (errors.length) {
+ throw errors[0];
+ }
+}
+
+function defaultFileName(task: TaskResult, useExternalId: boolean): string {
+ const base = useExternalId && task.external_id ? task.external_id : task.task_id;
+ return base.replace(/[^a-zA-Z0-9._-]/g, "_");
+}
+
+export interface DownloadToDirOptions {
+ status?: string;
+ nameFn?: (task: TaskResult) => string;
+ useExternalId?: boolean;
+ concurrency?: number;
+ maxFiles?: number;
+ maxBytesPerFile?: number;
+}
+
+/** Write every task body in `results` to `targetDir`, one file per task. */
+export async function downloadToDir(
+ results: AsyncIterable,
+ targetDir: string,
+ {
+ nameFn,
+ useExternalId = false,
+ concurrency = 1,
+ maxFiles = DEFAULT_MAX_FILES,
+ maxBytesPerFile = DEFAULT_MAX_BYTES_PER_FILE,
+ }: DownloadToDirOptions = {},
+): Promise {
+ await mkdir(targetDir, { recursive: true });
+ const tasks: TaskResult[] = [];
+ for await (const task of results) {
+ tasks.push(task);
+ if (tasks.length > maxFiles) {
+ throw new Error(`result count exceeds maxFiles (${maxFiles})`);
+ }
+ }
+
+ await runPool(tasks, concurrency, async (task: TaskResult) => {
+ const body = await fetchResultBody(task, maxBytesPerFile);
+ const fileName = nameFn ? nameFn(task) : defaultFileName(task, useExternalId);
+ const filePath = join(targetDir, fileName);
+ await mkdir(dirname(filePath), { recursive: true });
+ await pipeline(Readable.from(body), createWriteStream(filePath));
+ });
+
+ return tasks.length;
+}
+
+export interface DownloadToMemoryOptions {
+ concurrency?: number;
+ maxCount?: number;
+ maxTotalBytes?: number;
+ maxBytesPerFile?: number;
+}
+
+/** Load every task body in `results` into memory. */
+export async function downloadToMemory(
+ results: AsyncIterable,
+ {
+ concurrency = 1,
+ maxCount = DEFAULT_MAX_COUNT_IN_MEMORY,
+ maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES_IN_MEMORY,
+ maxBytesPerFile = DEFAULT_MAX_BYTES_PER_FILE,
+ }: DownloadToMemoryOptions = {},
+): Promise {
+ const tasks: TaskResult[] = [];
+ for await (const task of results) {
+ tasks.push(task);
+ if (tasks.length > maxCount) {
+ throw new Error(`result count exceeds maxCount (${maxCount})`);
+ }
+ }
+
+ const downloaded: DownloadedResult[] = new Array(tasks.length);
+ let totalBytes = 0;
+ await runPool(tasks, concurrency, async (task: TaskResult, index: number) => {
+ const body = await fetchResultBody(task, maxBytesPerFile);
+ totalBytes += body.byteLength;
+ if (totalBytes > maxTotalBytes) {
+ throw new Error(`total downloaded bytes exceeds maxTotalBytes (${maxTotalBytes})`);
+ }
+ downloaded[index] = { taskId: task.task_id, externalId: task.external_id, url: task.url, body };
+ });
+
+ return downloaded;
+}
+
+export async function downloadTaskToFile(task: TaskResult, target: string): Promise {
+ const body = await fetchResultBody(task);
+ await mkdir(dirname(target), { recursive: true });
+ await pipeline(Readable.from(body), createWriteStream(target));
+}
+
+export async function downloadTaskToMemory(task: TaskResult): Promise {
+ return fetchResultBody(task);
+}
diff --git a/src/batch/errors.ts b/src/batch/errors.ts
new file mode 100644
index 0000000..3108da9
--- /dev/null
+++ b/src/batch/errors.ts
@@ -0,0 +1,62 @@
+import type { ProblemJson } from "./types.js";
+
+/**
+ * A non-2xx response from the Batch API, decoded as RFC 7807 `application/problem+json`
+ * where possible. `code` is the stable Problem `code` member (e.g. `file_input_not_found`) —
+ * safe to branch on; defaults to `"internal"` when the body wasn't valid Problem JSON.
+ */
+export class ZenRowsBatchError extends Error {
+ readonly status: number;
+ readonly problem: ProblemJson | undefined;
+ readonly code: string;
+ readonly extras: Record | undefined;
+
+ constructor(status: number, problem: ProblemJson | undefined, extras?: Record) {
+ const message = problem
+ ? `${status} ${problem.title ?? "Error"}: ${problem.detail ?? problem.code ?? "unknown"}`
+ : `${status} (no problem body)`;
+ super(message);
+ this.name = "ZenRowsBatchError";
+ this.status = status;
+ this.problem = problem;
+ this.code = problem?.code ?? "internal";
+ this.extras = extras;
+ }
+}
+
+const PROBLEM_STANDARD_KEYS = new Set(["type", "title", "status", "code", "detail", "instance"]);
+
+/**
+ * Parse a Problem+JSON error body. Tolerant of non-JSON bodies (production servers
+ * occasionally return a raw error page, e.g. from an edge proxy) — degrades to
+ * `undefined` rather than throwing.
+ */
+export async function parseProblem(
+ response: Response,
+): Promise<{ problem: ProblemJson | undefined; extras: Record | undefined }> {
+ let body: unknown;
+ try {
+ body = await response.json();
+ } catch {
+ return { problem: undefined, extras: undefined };
+ }
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
+ return { problem: undefined, extras: undefined };
+ }
+ const record = body as Record;
+ const extras: Record = {};
+ for (const [key, value] of Object.entries(record)) {
+ if (!PROBLEM_STANDARD_KEYS.has(key)) {
+ extras[key] = value;
+ }
+ }
+ const problem: ProblemJson = {
+ type: typeof record.type === "string" ? record.type : "about:blank",
+ title: typeof record.title === "string" ? record.title : "Error",
+ status: typeof record.status === "number" ? record.status : response.status,
+ code: typeof record.code === "string" ? record.code : "internal",
+ detail: typeof record.detail === "string" ? record.detail : undefined,
+ instance: typeof record.instance === "string" ? record.instance : undefined,
+ };
+ return { problem, extras: Object.keys(extras).length ? extras : undefined };
+}
diff --git a/src/batch/estimate.ts b/src/batch/estimate.ts
new file mode 100644
index 0000000..8848154
--- /dev/null
+++ b/src/batch/estimate.ts
@@ -0,0 +1,131 @@
+/**
+ * Client-side cost estimation, mirroring the Python SDK's rate card exactly. Pure — no
+ * network call. Prices a job assuming every task succeeds once; `mode=auto` is billed
+ * dynamically post-factum and is reported as a [1, 25] credit range, everything else is exact.
+ */
+
+export const BASE_CREDITS = 1;
+export const JS_CREDITS = 5;
+export const PREMIUM_PROXY_CREDITS = 10;
+export const JS_AND_PROXY_CREDITS = 25;
+export const AUTO_MIN_CREDITS = 1;
+export const AUTO_MAX_CREDITS = 25;
+
+export type Tier = "base" | "js_render" | "premium_proxy" | "js_render+premium_proxy" | "auto";
+
+const TIER_ORDER: Tier[] = [
+ "base",
+ "js_render",
+ "premium_proxy",
+ "js_render+premium_proxy",
+ "auto",
+];
+
+const TRUTHY_STRINGS = new Set(["true", "1", "yes", "on"]);
+
+export type ParamValue = string | boolean | number | Record;
+export type ParamMap = Record;
+
+export interface EstimateTask {
+ zenrows_params?: ParamMap;
+}
+export type TaskLike = string | EstimateTask | Record;
+
+export interface CostLine {
+ tier: Tier;
+ count: number;
+ unitMin: number;
+ unitMax: number;
+}
+
+export interface CostEstimate {
+ taskCount: number;
+ min: number;
+ max: number;
+ breakdown: CostLine[];
+ exact: boolean;
+}
+
+function truthy(value: ParamValue | undefined): boolean {
+ if (typeof value === "boolean") return value;
+ if (typeof value === "number") return value !== 0;
+ if (typeof value === "string") return TRUTHY_STRINGS.has(value.trim().toLowerCase());
+ return false;
+}
+
+function isAuto(params: ParamMap): boolean {
+ return (
+ String(params.mode ?? "")
+ .trim()
+ .toLowerCase() === "auto"
+ );
+}
+
+function costForParams(params: ParamMap): { tier: Tier; min: number; max: number } {
+ if (isAuto(params)) {
+ return { tier: "auto", min: AUTO_MIN_CREDITS, max: AUTO_MAX_CREDITS };
+ }
+ const js = truthy(params.js_render);
+ const px = truthy(params.premium_proxy);
+ if (js && px) {
+ return {
+ tier: "js_render+premium_proxy",
+ min: JS_AND_PROXY_CREDITS,
+ max: JS_AND_PROXY_CREDITS,
+ };
+ }
+ if (px) {
+ return { tier: "premium_proxy", min: PREMIUM_PROXY_CREDITS, max: PREMIUM_PROXY_CREDITS };
+ }
+ if (js) {
+ return { tier: "js_render", min: JS_CREDITS, max: JS_CREDITS };
+ }
+ return { tier: "base", min: BASE_CREDITS, max: BASE_CREDITS };
+}
+
+function taskParams(task: TaskLike): ParamMap {
+ if (typeof task === "string") return {};
+ const record = task as Record;
+ return (record.zenrows_params as ParamMap | undefined) ?? {};
+}
+
+/**
+ * Estimate the credit cost of a job before submitting it. `tasks` accepts the same shapes
+ * `submitRegular` does: bare URL strings or task objects. Per-task `zenrows_params` override
+ * the job-level `zenrowsParams` on key collision (task wins), matching the worker's merge.
+ */
+export function estimateCost(tasks: Iterable, zenrowsParams?: ParamMap): CostEstimate {
+ const jobParams = zenrowsParams ?? {};
+ const agg = new Map();
+ let totalMin = 0;
+ let totalMax = 0;
+ let count = 0;
+
+ for (const task of tasks) {
+ count += 1;
+ const merged = { ...jobParams, ...taskParams(task) };
+ const cost = costForParams(merged);
+ totalMin += cost.min;
+ totalMax += cost.max;
+ const existing = agg.get(cost.tier);
+ if (existing) {
+ existing.count += 1;
+ } else {
+ agg.set(cost.tier, { count: 1, min: cost.min, max: cost.max });
+ }
+ }
+
+ const breakdown: CostLine[] = TIER_ORDER.filter((tier) => agg.has(tier)).map((tier) => {
+ const entry = agg.get(tier);
+ if (!entry) throw new Error("unreachable");
+ return { tier, count: entry.count, unitMin: entry.min, unitMax: entry.max };
+ });
+
+ return {
+ taskCount: count,
+ min: totalMin,
+ max: totalMax,
+ breakdown,
+ exact: totalMin === totalMax,
+ };
+}
diff --git a/src/batch/resources.ts b/src/batch/resources.ts
new file mode 100644
index 0000000..39dee1d
--- /dev/null
+++ b/src/batch/resources.ts
@@ -0,0 +1,497 @@
+import type { ZenRowsBatchClient } from "./client.js";
+import type {
+ DownloadToDirOptions,
+ DownloadToMemoryOptions,
+ DownloadedResult,
+} from "./download.js";
+import type { Schedule } from "./schedule.js";
+import type {
+ Export,
+ ExportStatus,
+ Job,
+ JobStatus,
+ Run,
+ RunStats,
+ RunStatus,
+ StartExportResponse,
+ SubmitJobResponse,
+ TaskHistoryResponse,
+ TaskResult,
+ WebhookConfig,
+} from "./types.js";
+
+interface BatchTaskLike {
+ url: string;
+ external_id?: string;
+ metadata?: Record;
+ zenrows_params?: Record;
+}
+
+// ======================= specific runs =======================
+
+/** A reference to a specific run by `(jobId, runId)`. No pause/stop — only the CURRENT run supports those. */
+export class RunRef {
+ #client: ZenRowsBatchClient;
+
+ constructor(
+ client: ZenRowsBatchClient,
+ readonly jobId: string,
+ readonly runId: string,
+ ) {
+ this.#client = client;
+ }
+
+ async load(): Promise {
+ return new RunHandle(
+ this.#client,
+ this.jobId,
+ this.runId,
+ await this.#client._getRunData(this.jobId, this.runId),
+ );
+ }
+
+ async delete(): Promise {
+ await this.#client._deleteRun(this.jobId, this.runId);
+ }
+
+ results(options: { status?: string } = {}): AsyncGenerator {
+ return this.#client.iterResults(this.jobId, { runId: this.runId, status: options.status });
+ }
+
+ taskHistory(taskId: string): Promise {
+ return this.#client.getTaskHistory(this.jobId, taskId, { runId: this.runId });
+ }
+
+ downloadToDir(
+ targetDir: string,
+ options: DownloadToDirOptions & { status?: string } = {},
+ ): Promise {
+ return this.#client.downloadToDir(this.jobId, this.runId, targetDir, {
+ status: "successful",
+ ...options,
+ });
+ }
+
+ downloadToMemory(
+ options: DownloadToMemoryOptions & { status?: string } = {},
+ ): Promise {
+ return this.#client.downloadToMemory(this.jobId, this.runId, {
+ status: "successful",
+ ...options,
+ });
+ }
+
+ downloadTaskToFile(task: TaskResult, target: string): Promise {
+ return this.#client.downloadTaskToFile(task, target);
+ }
+
+ downloadTaskToMemory(task: TaskResult): Promise {
+ return this.#client.downloadTaskToMemory(task);
+ }
+
+ async wait(
+ options: {
+ targetStatuses?: Set;
+ failureStatuses?: Set;
+ timeout?: number;
+ pollInterval?: number;
+ } = {},
+ ): Promise {
+ const run = await this.#client.waitForRun(this.jobId, { ...options, runId: this.runId });
+ return new RunHandle(this.#client, this.jobId, this.runId, run);
+ }
+
+ startExport(): Promise {
+ return this.#client.startResultsExport(this.jobId, this.runId);
+ }
+
+ export(exportId: string): ExportRef {
+ return new ExportRef(this.#client, this.jobId, this.runId, exportId);
+ }
+
+ downloadAllResults(
+ targetPath: string,
+ options: { waitTimeout?: number; pollInterval?: number } = {},
+ ): Promise {
+ return this.#client.downloadAllResults(this.jobId, this.runId, targetPath, options);
+ }
+}
+
+export class RunHandle extends RunRef {
+ constructor(
+ client: ZenRowsBatchClient,
+ jobId: string,
+ runId: string,
+ readonly data: Run,
+ ) {
+ super(client, jobId, runId);
+ }
+
+ get status(): RunStatus {
+ return this.data.status;
+ }
+
+ get stats(): RunStats {
+ return this.data.stats;
+ }
+}
+
+// ============================ exports ============================
+
+export class ExportRef {
+ #client: ZenRowsBatchClient;
+
+ constructor(
+ client: ZenRowsBatchClient,
+ readonly jobId: string,
+ readonly runId: string,
+ readonly exportId: string,
+ readonly startResponse?: StartExportResponse,
+ ) {
+ this.#client = client;
+ }
+
+ async load(): Promise {
+ const data = await this.#client._getExport(this.jobId, this.runId, this.exportId);
+ return new ExportHandle(
+ this.#client,
+ this.jobId,
+ this.runId,
+ this.exportId,
+ data,
+ this.startResponse,
+ );
+ }
+
+ async wait(
+ options: { targetStatuses?: Set; timeout?: number; pollInterval?: number } = {},
+ ): Promise {
+ const data = await this.#client.waitForExport(this.jobId, this.runId, this.exportId, options);
+ return new ExportHandle(
+ this.#client,
+ this.jobId,
+ this.runId,
+ this.exportId,
+ data,
+ this.startResponse,
+ );
+ }
+
+ /** Stream the export zip to `targetPath`. The export must already be `completed`. */
+ async downloadToPath(targetPath: string): Promise {
+ const handle = await this.load();
+ if (handle.data.status !== "completed" || !handle.data.download_url) {
+ throw new Error(handle.data.error ?? "export not completed");
+ }
+ const { mkdir } = await import("node:fs/promises");
+ const { dirname } = await import("node:path");
+ const { createWriteStream } = await import("node:fs");
+ const { Readable } = await import("node:stream");
+ const { pipeline } = await import("node:stream/promises");
+ await mkdir(dirname(targetPath), { recursive: true });
+ const response = await fetch(handle.data.download_url);
+ if (!response.ok) {
+ throw new Error(`export download failed with status ${response.status}`);
+ }
+ await pipeline(Readable.fromWeb(response.body as never), createWriteStream(targetPath));
+ return targetPath;
+ }
+}
+
+export class ExportHandle extends ExportRef {
+ constructor(
+ client: ZenRowsBatchClient,
+ jobId: string,
+ runId: string,
+ exportId: string,
+ readonly data: Export,
+ startResponse?: StartExportResponse,
+ ) {
+ super(client, jobId, runId, exportId, startResponse);
+ }
+
+ get status(): ExportStatus {
+ return this.data.status;
+ }
+}
+
+// ===================== current-run facet =====================
+
+/** Operations on a job's CURRENT run, reached via `job.run`. */
+export class CurrentRun {
+ #client: ZenRowsBatchClient;
+
+ constructor(
+ client: ZenRowsBatchClient,
+ readonly jobId: string,
+ ) {
+ this.#client = client;
+ }
+
+ private async currentRunId(): Promise {
+ const job = await this.#client._getJobData(this.jobId);
+ if (!job.latest_run) {
+ throw new Error(`job ${this.jobId} has no run yet`);
+ }
+ return job.latest_run.run_id;
+ }
+
+ async load(): Promise {
+ const job = await this.#client._getJobData(this.jobId);
+ if (!job.latest_run) {
+ throw new Error(`job ${this.jobId} has no run yet`);
+ }
+ return new RunHandle(this.#client, this.jobId, job.latest_run.run_id, job.latest_run);
+ }
+
+ async pause(): Promise {
+ const run = await this.#client._postPause(this.jobId);
+ return new RunHandle(this.#client, this.jobId, run.run_id, run);
+ }
+
+ async resume(): Promise {
+ const run = await this.#client._postResume(this.jobId);
+ return new RunHandle(this.#client, this.jobId, run.run_id, run);
+ }
+
+ async stop(): Promise {
+ const run = await this.#client._postStop(this.jobId);
+ return new RunHandle(this.#client, this.jobId, run.run_id, run);
+ }
+
+ cancel(): Promise {
+ return this.stop();
+ }
+
+ async wait(
+ options: {
+ targetStatuses?: Set;
+ failureStatuses?: Set;
+ timeout?: number;
+ pollInterval?: number;
+ } = {},
+ ): Promise {
+ const run = await this.#client.waitForRun(this.jobId, options);
+ return new RunHandle(this.#client, this.jobId, run.run_id, run);
+ }
+
+ results(options: { status?: string } = {}): AsyncGenerator {
+ return this.#client.iterResults(this.jobId, { status: options.status });
+ }
+
+ taskHistory(taskId: string): Promise {
+ return this.#client.getTaskHistory(this.jobId, taskId);
+ }
+
+ downloadToDir(
+ targetDir: string,
+ options: DownloadToDirOptions & { status?: string } = {},
+ ): Promise {
+ return this.#client.downloadToDir(this.jobId, undefined, targetDir, {
+ status: "successful",
+ ...options,
+ });
+ }
+
+ downloadToMemory(
+ options: DownloadToMemoryOptions & { status?: string } = {},
+ ): Promise {
+ return this.#client.downloadToMemory(this.jobId, undefined, {
+ status: "successful",
+ ...options,
+ });
+ }
+
+ downloadTaskToFile(task: TaskResult, target: string): Promise {
+ return this.#client.downloadTaskToFile(task, target);
+ }
+
+ downloadTaskToMemory(task: TaskResult): Promise {
+ return this.#client.downloadTaskToMemory(task);
+ }
+
+ async startExport(): Promise {
+ return this.#client.startResultsExport(this.jobId, await this.currentRunId());
+ }
+
+ async downloadAllResults(
+ targetPath: string,
+ options: { waitTimeout?: number; pollInterval?: number } = {},
+ ): Promise {
+ return this.#client.downloadAllResults(
+ this.jobId,
+ await this.currentRunId(),
+ targetPath,
+ options,
+ );
+ }
+}
+
+// ===================== schedule facet =====================
+
+/** Operations on a scheduled job's schedule, reached via `job.schedule`. Scheduled jobs only — regular jobs 409. */
+export class ScheduleControls {
+ #client: ZenRowsBatchClient;
+
+ constructor(
+ client: ZenRowsBatchClient,
+ readonly jobId: string,
+ ) {
+ this.#client = client;
+ }
+
+ async pause(): Promise {
+ return new JobHandle(
+ this.#client,
+ this.jobId,
+ await this.#client._postScheduleState(this.jobId, "paused"),
+ );
+ }
+
+ async resume(): Promise {
+ return new JobHandle(
+ this.#client,
+ this.jobId,
+ await this.#client._postScheduleState(this.jobId, "active"),
+ );
+ }
+
+ async update(schedule: Schedule): Promise {
+ return new JobHandle(
+ this.#client,
+ this.jobId,
+ await this.#client._putSchedule(this.jobId, schedule.toRequestBody()),
+ );
+ }
+}
+
+// ============================ jobs ============================
+
+/** A reference to a job by id — job-template operations, plus `run`/`schedule` sub-facets. */
+export class JobRef {
+ #client: ZenRowsBatchClient;
+ private runFacet?: CurrentRun;
+ private scheduleFacet?: ScheduleControls;
+
+ constructor(
+ client: ZenRowsBatchClient,
+ readonly jobId: string,
+ readonly submitResponse?: SubmitJobResponse,
+ ) {
+ this.#client = client;
+ }
+
+ /** Operations on the CURRENT run — pause/resume/stop/wait/results/downloads/startExport. */
+ get run(): CurrentRun {
+ if (!this.runFacet) this.runFacet = new CurrentRun(this.#client, this.jobId);
+ return this.runFacet;
+ }
+
+ /** Operations on the schedule — pause/resume/update (scheduled jobs only). */
+ get schedule(): ScheduleControls {
+ if (!this.scheduleFacet) this.scheduleFacet = new ScheduleControls(this.#client, this.jobId);
+ return this.scheduleFacet;
+ }
+
+ /** The job status from the submit response — only known on refs from a `submit*` call. */
+ get status(): JobStatus | undefined {
+ return this.submitResponse?.status;
+ }
+
+ get acceptedTasks(): number | undefined {
+ return this.submitResponse?.accepted_tasks;
+ }
+
+ async load(): Promise {
+ return new JobHandle(this.#client, this.jobId, await this.#client._getJobData(this.jobId));
+ }
+
+ async close(): Promise {
+ return new JobHandle(this.#client, this.jobId, await this.#client._postClose(this.jobId));
+ }
+
+ async delete(): Promise {
+ await this.#client._delete(this.jobId);
+ }
+
+ async rerun(
+ options: { status?: string | string[]; idempotencyKey?: string } = {},
+ ): Promise {
+ const resp = await this.#client._postRerun(this.jobId, options);
+ return new RunHandle(this.#client, this.jobId, resp.latest_run.run_id, resp.latest_run);
+ }
+
+ /** Shortcut for `rerun({ status: "failed" })` (or `"failed,pending"` with `includePending`). */
+ retryFailed(
+ options: { includePending?: boolean; idempotencyKey?: string } = {},
+ ): Promise {
+ const status = options.includePending ? ["failed", "pending"] : "failed";
+ return this.rerun({ status, idempotencyKey: options.idempotencyKey });
+ }
+
+ addTasks(
+ tasks: BatchTaskLike[],
+ options: { lastBatch?: boolean } = {},
+ ): Promise<{
+ accepted_tasks: number;
+ job_status: JobStatus;
+ latest_run: Run;
+ }> {
+ return this.#client._postTasks(this.jobId, tasks, options);
+ }
+
+ async *runs(pageSize?: number): AsyncGenerator {
+ yield* this.#client.iterRuns(this.jobId, pageSize);
+ }
+
+ addFileInput(
+ data: Uint8Array | string,
+ options: {
+ fields: { url: string | number; external_id?: string | number };
+ header?: boolean;
+ delimiter?: string;
+ quote?: string;
+ },
+ ): Promise {
+ return this.#client.uploadCsv(data, options);
+ }
+
+ getWebhook(): Promise {
+ return this.#client.getJobWebhook(this.jobId);
+ }
+
+ setWebhook(url: string, signature: boolean): Promise {
+ return this.#client.putJobWebhook(this.jobId, { url, signature });
+ }
+
+ async deleteWebhook(): Promise {
+ await this.#client.deleteJobWebhook(this.jobId);
+ }
+
+ /** Block until the current run's async-carrier ingestion has finished writing task rows. */
+ async waitForIngest(
+ options: { timeout?: number; pollInterval?: number; maxPollInterval?: number } = {},
+ ): Promise {
+ return new JobHandle(
+ this.#client,
+ this.jobId,
+ await this.#client._waitForIngestRaw(this.jobId, options),
+ this.submitResponse,
+ );
+ }
+}
+
+export class JobHandle extends JobRef {
+ constructor(
+ client: ZenRowsBatchClient,
+ jobId: string,
+ readonly data: Job,
+ submitResponse?: SubmitJobResponse,
+ ) {
+ super(client, jobId, submitResponse);
+ }
+
+ get status(): JobStatus {
+ return this.data.status;
+ }
+}
diff --git a/src/batch/schedule.ts b/src/batch/schedule.ts
new file mode 100644
index 0000000..d519bff
--- /dev/null
+++ b/src/batch/schedule.ts
@@ -0,0 +1,194 @@
+import type { JobSchedule } from "./types.js";
+
+const DAYS_OF_WEEK = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
+type DayOfWeek = (typeof DAYS_OF_WEEK)[number];
+
+function validateTimezone(tz: string, field: string): void {
+ if (!tz) {
+ throw new Error(`${field} is required (IANA name, e.g. "Europe/Berlin")`);
+ }
+ try {
+ new Intl.DateTimeFormat(undefined, { timeZone: tz });
+ } catch {
+ throw new Error(`${field}: ${JSON.stringify(tz)} is not a valid IANA timezone`);
+ }
+}
+
+function validateFullHour(s: string): void {
+ if (s.length !== 5 || s[2] !== ":" || s.slice(3) !== "00") {
+ throw new Error(
+ `times_of_day entry ${JSON.stringify(s)} must be on the hour ("HH:00"); minute granularity is rejected.`,
+ );
+ }
+ const h = Number(s.slice(0, 2));
+ if (!Number.isInteger(h) || h < 0 || h > 23) {
+ throw new Error(`times_of_day entry ${JSON.stringify(s)} is not a valid hour (00..23)`);
+ }
+}
+
+/** True if `raw`'s time portion carries an RFC 3339-style tz tail (`Z` / `+HH:MM` / `-HH:MM`). */
+function hasTzSuffix(raw: string): boolean {
+ const trimmed = raw.trim();
+ for (const sep of ["T", " "]) {
+ const i = trimmed.indexOf(sep);
+ if (i >= 0) {
+ const tail = trimmed.slice(i + 1);
+ if (tail.endsWith("Z") || tail.endsWith("z")) return true;
+ return tail.length >= 6 && (tail[tail.length - 6] === "+" || tail[tail.length - 6] === "-");
+ }
+ }
+ return false;
+}
+
+/** Format a Date using its LOCAL wall-clock fields (not UTC) — see `At`'s doc for why. */
+function formatLocalNaive(date: Date): string {
+ const pad = (n: number, width = 2) => String(n).padStart(width, "0");
+ return (
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
+ `T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
+ );
+}
+
+/**
+ * One-shot fire at a specific wall-clock time. `at` must be tz-naive: a plain ISO string
+ * with no `Z`/offset suffix, or a `Date` — in which case its LOCAL getters (not UTC) are
+ * read as the intended wall-clock time. `timezone` is the single authoritative interpreter,
+ * which is what keeps DST transitions deterministic; passing a `Date` does NOT mean "this
+ * instant" — it means "this wall-clock reading, in whatever zone `timezone` names."
+ */
+export class At {
+ readonly at: string;
+ readonly timezone: string;
+
+ constructor(at: string | Date, timezone: string) {
+ validateTimezone(timezone, "At.timezone");
+ if (at instanceof Date) {
+ this.at = formatLocalNaive(at);
+ } else {
+ if (!at.trim()) {
+ throw new Error("At.at must be a non-empty ISO timestamp string.");
+ }
+ if (hasTzSuffix(at)) {
+ throw new Error(
+ `At.at ${JSON.stringify(at)} must be tz-naive (no "Z", no offset). Supply timezone separately to keep DST transitions deterministic.`,
+ );
+ }
+ this.at = at;
+ }
+ this.timezone = timezone;
+ }
+
+ toRequestBody(): JobSchedule {
+ return { at: this.at, timezone: this.timezone };
+ }
+}
+
+/** Interval-based fire policy — every N units, no alignment to wall clock. */
+export class Rate {
+ readonly every: number;
+ readonly unit: "minute" | "hour" | "day";
+
+ constructor(every: number, unit: "minute" | "hour" | "day") {
+ if (!Number.isInteger(every)) {
+ throw new TypeError(`Rate.every must be an integer, got ${every}`);
+ }
+ if (every < 1) {
+ throw new Error(`Rate.every must be >= 1, got ${every}`);
+ }
+ if (unit !== "minute" && unit !== "hour" && unit !== "day") {
+ throw new Error(
+ `Rate.unit must be one of "minute", "hour", "day"; got ${JSON.stringify(unit)}`,
+ );
+ }
+ this.every = every;
+ this.unit = unit;
+ }
+
+ toRequestBody(): JobSchedule {
+ return { rate: { every: this.every, unit: this.unit } };
+ }
+}
+
+/** Fire every day. No knobs. */
+export class Daily {}
+
+/** Fire on specific days of the week. `days` must be non-empty, lower-case 3-letter names. */
+export class Weekly {
+ readonly days: DayOfWeek[];
+
+ constructor(days: DayOfWeek[]) {
+ if (!days.length) {
+ throw new Error("Weekly.days must be non-empty");
+ }
+ for (const d of days) {
+ if (!DAYS_OF_WEEK.includes(d)) {
+ throw new Error(
+ `Weekly.days entry ${JSON.stringify(d)} is not a valid day (use one of ${DAYS_OF_WEEK.join(", ")})`,
+ );
+ }
+ }
+ this.days = days;
+ }
+}
+
+/** Fire on specific days of the month (1-31); days that don't exist in a given month are skipped. */
+export class Monthly {
+ readonly days: number[];
+
+ constructor(days: number[]) {
+ if (!days.length) {
+ throw new Error("Monthly.days must be non-empty");
+ }
+ for (const d of days) {
+ if (!Number.isInteger(d)) {
+ throw new TypeError(`Monthly.days entries must be integers, got ${d}`);
+ }
+ if (d < 1 || d > 31) {
+ throw new Error(`Monthly.days entry ${d} is out of range (1..31)`);
+ }
+ }
+ this.days = days;
+ }
+}
+
+export type Cadence = Daily | Weekly | Monthly;
+
+/** Calendar-style fire policy: a list of times-of-day on a daily/weekly/monthly cadence. */
+export class Calendar {
+ readonly timesOfDay: string[];
+ readonly cadence: Cadence;
+ readonly timezone: string;
+
+ constructor(timesOfDay: string[], cadence: Cadence, timezone: string) {
+ validateTimezone(timezone, "Calendar.timezone");
+ if (!timesOfDay.length) {
+ throw new Error("Calendar.times_of_day must be non-empty");
+ }
+ for (const t of timesOfDay) {
+ validateFullHour(t);
+ }
+ this.timesOfDay = timesOfDay;
+ this.cadence = cadence;
+ this.timezone = timezone;
+ }
+
+ toRequestBody(): JobSchedule {
+ let cadenceBody:
+ | { daily: object }
+ | { weekly: { days: string[] } }
+ | { monthly: { days: number[] } };
+ if (this.cadence instanceof Daily) {
+ cadenceBody = { daily: {} };
+ } else if (this.cadence instanceof Weekly) {
+ cadenceBody = { weekly: { days: [...this.cadence.days] } };
+ } else {
+ cadenceBody = { monthly: { days: [...this.cadence.days] } };
+ }
+ return {
+ calendar: { times_of_day: [...this.timesOfDay], cadence: cadenceBody },
+ timezone: this.timezone,
+ };
+ }
+}
+
+export type Schedule = At | Rate | Calendar;
diff --git a/src/batch/transport.ts b/src/batch/transport.ts
new file mode 100644
index 0000000..d8328dd
--- /dev/null
+++ b/src/batch/transport.ts
@@ -0,0 +1,135 @@
+import packageJson from "../../package.json" with { type: "json" };
+import { ZenRowsBatchError, parseProblem } from "./errors.js";
+
+const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
+const IDEMPOTENT_METHODS = new Set(["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]);
+
+const BACKOFF_BASE_MS = 250;
+const BACKOFF_CAP_MS = 10_000;
+const DEFAULT_RETRIES = 3;
+
+function isIdempotent(method: string, hasIdempotencyKey: boolean): boolean {
+ return (
+ IDEMPOTENT_METHODS.has(method.toUpperCase()) ||
+ (method.toUpperCase() === "POST" && hasIdempotencyKey)
+ );
+}
+
+/** Jittered exponential backoff for retry `attempt` (0-based), in milliseconds. */
+function backoffMs(attempt: number): number {
+ const base = Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_CAP_MS);
+ return base * (1 + (Math.random() * 2 - 1) * 0.2);
+}
+
+function retryAfterMs(response: Response): number | undefined {
+ const raw = response.headers.get("Retry-After");
+ if (!raw) return undefined;
+ const secs = Number(raw);
+ if (Number.isNaN(secs) || secs < 0) return undefined;
+ return secs * 1000;
+}
+
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export interface RequestOptions {
+ query?: object;
+ body?: unknown;
+ headers?: Record;
+ idempotencyKey?: string;
+}
+
+/**
+ * Thin HTTP transport for the Batch API: `X-API-Key` auth, retries for transient failures
+ * (429/502/503/504 + network errors) on idempotent requests only, and RFC 7807 → `ZenRowsBatchError`
+ * mapping. Mirrors the Python SDK's `_transport.py` exactly (250ms · 2^attempt backoff, ±20%
+ * jitter, capped at 10s, honors `Retry-After`).
+ */
+export class BatchTransport {
+ constructor(
+ private readonly baseURL: string,
+ private readonly apiKey: string,
+ private readonly retries: number = DEFAULT_RETRIES,
+ ) {}
+
+ /** Send a request, parse the response, throw `ZenRowsBatchError` on non-2xx. */
+ async requestJson(method: string, path: string, options: RequestOptions = {}): Promise {
+ const response = await this.send(method, path, options);
+ if (!response.ok) {
+ const { problem, extras } = await parseProblem(response);
+ throw new ZenRowsBatchError(response.status, problem, extras);
+ }
+ const text = await response.text();
+ if (!text) {
+ return undefined as T;
+ }
+ return JSON.parse(text) as T;
+ }
+
+ /** Send a request and return the raw `Response` — used for endpoints that don't return JSON. */
+ async requestRaw(method: string, path: string, options: RequestOptions = {}): Promise {
+ const response = await this.send(method, path, options);
+ if (!response.ok) {
+ const { problem, extras } = await parseProblem(response);
+ throw new ZenRowsBatchError(response.status, problem, extras);
+ }
+ return response;
+ }
+
+ private buildUrl(path: string, query?: object): URL {
+ const url = new URL(`${this.baseURL}${path}`);
+ if (query) {
+ for (const [key, value] of Object.entries(query as Record)) {
+ if (value !== undefined) {
+ url.searchParams.append(key, String(value));
+ }
+ }
+ }
+ return url;
+ }
+
+ private async send(method: string, path: string, options: RequestOptions): Promise {
+ const url = this.buildUrl(path, options.query);
+ const headers: Record = {
+ "X-API-Key": this.apiKey,
+ "User-Agent": `zenrows/${packageJson.version} node`,
+ ...options.headers,
+ };
+ if (options.body !== undefined) {
+ headers["Content-Type"] = "application/json";
+ }
+ if (options.idempotencyKey) {
+ headers["Idempotency-Key"] = options.idempotencyKey;
+ }
+
+ const idempotent = isIdempotent(method, Boolean(options.idempotencyKey));
+ let attempt = 0;
+ while (true) {
+ let response: Response;
+ try {
+ response = await fetch(url.toString(), {
+ method,
+ headers,
+ body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
+ });
+ } catch (error) {
+ if (idempotent && attempt < this.retries) {
+ await sleep(backoffMs(attempt));
+ attempt += 1;
+ continue;
+ }
+ throw error;
+ }
+
+ if (idempotent && attempt < this.retries && RETRYABLE_STATUSES.has(response.status)) {
+ const wait = retryAfterMs(response) ?? backoffMs(attempt);
+ await sleep(wait);
+ attempt += 1;
+ continue;
+ }
+
+ return response;
+ }
+ }
+}
diff --git a/src/batch/types.ts b/src/batch/types.ts
new file mode 100644
index 0000000..63b76d9
--- /dev/null
+++ b/src/batch/types.ts
@@ -0,0 +1,206 @@
+export type JobType = "regular" | "scheduled";
+export type JobStatus = "open" | "closed" | "deleted";
+export type ScheduleState = "active" | "paused";
+export type RunStatus = "pending" | "running" | "completed" | "failed" | "stopped" | "deleted";
+export type TaskStatus = "pending" | "successful" | "failed";
+export type PauseState = "active" | "paused";
+export type IngestStatus = "pending" | "done";
+export type ExportStatus = "pending" | "running" | "completed" | "failed";
+
+export interface Spend {
+ credits: number;
+ cost: number;
+}
+
+export interface TaskSpend {
+ total: Spend;
+ last_attempt: Spend;
+}
+
+export interface RunStats {
+ total: number;
+ completed: number;
+ successful: number;
+ failed: number;
+ failure_reasons?: Record;
+ spend?: Spend;
+}
+
+export interface WebhookConfig {
+ url: string;
+ signature: boolean;
+}
+
+export interface JobSchedule {
+ at?: string;
+ rate?: { every: number; unit: "minute" | "hour" | "day" };
+ calendar?: {
+ times_of_day: string[];
+ cadence: { daily?: object; weekly?: { days: string[] }; monthly?: { days: number[] } };
+ };
+ timezone?: string;
+}
+
+export interface Run {
+ run_id: string;
+ job_id: string;
+ run_sequence: number;
+ status: RunStatus;
+ stats: RunStats;
+ last_batch_received?: boolean;
+ pause_state?: PauseState;
+ ingest_status?: IngestStatus;
+ failure_reason?: "insufficient_credits" | "subscription_inactive";
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface Job {
+ job_id: string;
+ type: JobType;
+ status: JobStatus;
+ format?: string;
+ zenrows_params?: Record;
+ external_id?: string;
+ name?: string;
+ metadata?: Record;
+ schedule?: JobSchedule;
+ next_scheduled_run?: string;
+ schedule_state?: ScheduleState;
+ webhook?: WebhookConfig;
+ latest_run?: Run;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface ListJobsResponse {
+ jobs: Job[];
+ next_cursor?: string;
+}
+
+export interface ListJobRunsResponse {
+ runs: Run[];
+ next_cursor?: string;
+}
+
+export interface SubmitJobResponse {
+ job_id: string;
+ status: JobStatus;
+ latest_run?: Run;
+ accepted_tasks: number;
+ webhook?: WebhookConfig;
+}
+
+export interface AddTasksResponse {
+ accepted_tasks: number;
+ job_status: JobStatus;
+ latest_run: Run;
+}
+
+export interface RerunJobResponse {
+ job_id: string;
+ status: JobStatus;
+ latest_run: Run;
+ rerun_of?: string;
+ retried_tasks: number;
+ inherited_tasks: number;
+}
+
+export interface ProblemJson {
+ type?: string;
+ title?: string;
+ status?: number;
+ code?: string;
+ detail?: string;
+ instance?: string;
+}
+
+export interface TaskResult {
+ task_id: string;
+ external_id?: string;
+ run_id: string;
+ url: string;
+ metadata?: Record;
+ method?: "GET" | "POST";
+ status: TaskStatus;
+ type?: string;
+ result_url?: string;
+ error?: ProblemJson;
+ source_run_id?: string;
+ spend?: TaskSpend;
+}
+
+export interface ListResultsResponse {
+ results: TaskResult[];
+ next_cursor?: string;
+}
+
+export interface TaskHistoryEvent {
+ started_at: string;
+ ended_at: string;
+ attempt: number;
+ error?: ProblemJson;
+ spend?: Spend;
+}
+
+export interface TaskHistoryResponse {
+ events: TaskHistoryEvent[];
+}
+
+export interface StartExportResponse {
+ export_id: string;
+ status: ExportStatus;
+ created_at: string;
+ expires_at: string;
+}
+
+export interface Export {
+ export_id: string;
+ status: ExportStatus;
+ error?: string;
+ download_url?: string;
+ created_at: string;
+ expires_at: string;
+}
+
+export interface FileInputUploadTarget {
+ method: "PUT";
+ url: string;
+ headers?: Record;
+ expires_at: string;
+}
+
+export interface CreateJobInputResponse {
+ file_input_id: string;
+ upload: FileInputUploadTarget;
+ expires_at: string;
+}
+
+export interface HMACKeyMeta {
+ kid: string;
+ created_at: string;
+}
+
+export interface HMACKeyList {
+ active?: HMACKeyMeta;
+ candidate?: HMACKeyMeta;
+}
+
+export interface HMACKeyCreated {
+ kid: string;
+ secret: string;
+ created_at: string;
+}
+
+export interface HMACKeyFinalized {
+ active_kid: string;
+ created_at: string;
+}
+
+export interface TestWebhookResponse {
+ delivered: boolean;
+ event_id: string;
+ status_code?: number;
+ error?: string;
+ elapsed_ms: number;
+}
diff --git a/src/batch/waiters.ts b/src/batch/waiters.ts
new file mode 100644
index 0000000..417f123
--- /dev/null
+++ b/src/batch/waiters.ts
@@ -0,0 +1,70 @@
+/** Raised when a waiter's `timeout` elapsed before the target state was reached. */
+export class WaiterTimeoutError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "WaiterTimeoutError";
+ }
+}
+
+/** Raised when the resource entered one of the caller's `isFailure` states. */
+export class WaiterFailureError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "WaiterFailureError";
+ }
+}
+
+export interface PollUntilOptions {
+ isDone(value: T): boolean;
+ isFailure?(value: T): boolean;
+ timeout?: number;
+ initialInterval?: number;
+ maxInterval?: number;
+ backoff?: number;
+ jitter?: number;
+}
+
+/**
+ * Generic poll loop. Calls `fetch()` repeatedly until `isDone(value)` (returns the value),
+ * `isFailure(value)` (throws `WaiterFailureError`), or `timeout` seconds elapse (throws
+ * `WaiterTimeoutError`). The wait between calls starts at `initialInterval`, multiplies by
+ * `backoff` each iteration, caps at `maxInterval`, and is jittered by ±`jitter` fraction so
+ * concurrent waiters don't synchronise into thundering-herd patterns against the API.
+ */
+export async function pollUntil(
+ fetch: () => Promise | T,
+ {
+ isDone,
+ isFailure,
+ timeout = 300,
+ initialInterval = 1,
+ maxInterval = 15,
+ backoff = 1.5,
+ jitter = 0.2,
+ }: PollUntilOptions,
+): Promise {
+ const deadline = Date.now() + timeout * 1000;
+ let interval = initialInterval;
+ while (true) {
+ const value = await fetch();
+ if (isDone(value)) {
+ return value;
+ }
+ if (isFailure?.(value)) {
+ throw new WaiterFailureError(
+ `waiter: resource entered failure state (${JSON.stringify(value)})`,
+ );
+ }
+ const now = Date.now();
+ if (now >= deadline) {
+ throw new WaiterTimeoutError(`waiter: timed out after ${timeout}s waiting for target state`);
+ }
+ const remainingSeconds = (deadline - now) / 1000;
+ const sleepSeconds =
+ Math.min(interval, remainingSeconds) * (1 + jitter * (Math.random() * 2 - 1));
+ if (sleepSeconds > 0) {
+ await new Promise((resolve) => setTimeout(resolve, sleepSeconds * 1000));
+ }
+ interval = Math.min(interval * backoff, maxInterval);
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index 33b1455..de02b6e 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,11 +1,35 @@
import fastq from "fastq";
import fetchRetry from "fetch-retry";
import packageJson from "../package.json" with { type: "json" };
+import { ZenRowsBatchClient } from "./batch/client.js";
+
+export * from "./batch/client.js";
+export * from "./batch/errors.js";
+export * from "./batch/estimate.js";
+export * from "./batch/schedule.js";
+export * from "./batch/waiters.js";
+export * from "./batch/download.js";
+export type * from "./batch/types.js";
const API_URL = "https://api.zenrows.com/v1/";
type HttpMethods = "GET" | "POST" | "PUT";
+/** Extract mode — see https://docs.zenrows.com for what each mode returns. */
+export type ExtractMode = "auto" | "native" | "standard";
+
+/** True when a response's JSON error envelope carries the Extract
+ * domain-not-enabled code (AUTH010). Reads via `.clone()` so the original
+ * response body is left unconsumed for the caller. */
+async function isAuth010(response: Response): Promise {
+ try {
+ const body = (await response.clone().json()) as { code?: unknown };
+ return typeof body?.code === "string" && body.code.toUpperCase() === "AUTH010";
+ } catch {
+ return false;
+ }
+}
+
interface ClientConfig {
concurrency?: number;
retries?: number;
@@ -36,10 +60,13 @@ export class ZenRows {
readonly clientConfig: ClientConfig;
readonly queue;
readonly fetchWithRetry;
+ /** Client for the Batch API (async job/run/task model) — see `./batch.ts`. */
+ readonly batch: ZenRowsBatchClient;
constructor(apiKey: string, clientConfig: ClientConfig = {}) {
this.apiKey = apiKey;
this.clientConfig = clientConfig;
+ this.batch = new ZenRowsBatchClient(apiKey);
const retries = this.clientConfig.retries ?? 0;
this.queue = fastq.promise(this, this.worker, this.clientConfig.concurrency ?? 5);
@@ -66,7 +93,11 @@ export class ZenRows {
});
}
- public get(
+ /**
+ * Fetch a URL through Zenrows — the main page-scraping product. This is the primary entry
+ * point; `get()` remains as a deprecated alias for existing callers.
+ */
+ public fetch(
url: string,
config?: ZenRowsConfig,
{ headers = {} }: { headers?: Headers } = {},
@@ -74,6 +105,61 @@ export class ZenRows {
return this.queue.push({ url, config, headers });
}
+ /** @deprecated Use `fetch()` instead. Kept for backward compatibility. */
+ public get(
+ url: string,
+ config?: ZenRowsConfig,
+ opts: { headers?: Headers } = {},
+ ): Promise {
+ return this.fetch(url, config, opts);
+ }
+
+ /**
+ * Fetch a URL and run it through Extract — Zenrows' AI-powered structured extraction
+ * (beta). `mode` defaults to `"auto"`; pass `"native"` or `"standard"` to pick a
+ * different extraction contract. This is a thin, typed wrapper over `fetch()` with the
+ * `extract` param set — no separate endpoint or auth.
+ *
+ * Also sends Adaptive Stealth Mode (`mode: "auto"`) by default, so a target that needs
+ * `js_render`/`premium_proxy` gets escalated automatically instead of failing with
+ * REQS002 — pass `adaptiveStealth: false` to disable that and set `js_render`/
+ * `premium_proxy` yourself.
+ *
+ * `extract=auto` is a domain-gated open beta: when the target domain isn't enabled yet,
+ * the API returns a 402 with `code: "AUTH010"`. By default this method catches that and
+ * retries once with `autoparse: true` instead of returning the error response — pass
+ * `fallbackToAutoparse: false` to disable that and get the raw AUTH010 response back.
+ */
+ public async extract(
+ url: string,
+ config?: ZenRowsConfig & {
+ extract?: ExtractMode;
+ fallbackToAutoparse?: boolean;
+ adaptiveStealth?: boolean;
+ },
+ opts: { headers?: Headers } = {},
+ ): Promise {
+ const {
+ extract = "auto",
+ fallbackToAutoparse = true,
+ adaptiveStealth = true,
+ ...rest
+ } = config ?? {};
+ const mode = adaptiveStealth ? "auto" : undefined;
+ const response = await this.fetch(url, { ...rest, extract, mode }, opts);
+
+ if (
+ response.status === 402 &&
+ extract === "auto" &&
+ fallbackToAutoparse &&
+ (await isAuth010(response))
+ ) {
+ return this.fetch(url, { ...rest, autoparse: true, mode }, opts);
+ }
+
+ return response;
+ }
+
public post(
url: string,
config?: ZenRowsConfig,
diff --git a/tests/_setup.ts b/tests/_setup.ts
index 2c563dc..afe50c9 100644
--- a/tests/_setup.ts
+++ b/tests/_setup.ts
@@ -12,6 +12,360 @@ const handlers = [
http.post("https://api.zenrows.com/v1/", () => {
return new HttpResponse();
}),
+ http.post("https://async.api.zenrows.com/v1/jobs", () => {
+ return HttpResponse.json({ job_id: "job_123", status: "open" }, { status: 201 });
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs", () => {
+ return HttpResponse.json({ jobs: [], next_cursor: null });
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123", () => {
+ return HttpResponse.json({ job_id: "job_123", status: "open" });
+ }),
+ http.post("https://async.api.zenrows.com/v1/jobs/job_123/tasks", () => {
+ return HttpResponse.json({ accepted_tasks: 1, job_status: "open" });
+ }),
+ http.post("https://async.api.zenrows.com/v1/jobs/job_123/close", () => {
+ return HttpResponse.json({ job_id: "job_123", status: "closed" });
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/results", () => {
+ return HttpResponse.json({ results: [], next_cursor: null });
+ }),
+ // `DELETE /jobs/{id}` is documented as 202 Accepted with an empty body (async delete).
+ http.delete("https://async.api.zenrows.com/v1/jobs/job_123", () => {
+ return new HttpResponse(null, { status: 202 });
+ }),
+ http.post("https://async.api.zenrows.com/v1/jobs/job_123/stop", () => {
+ return HttpResponse.json({ run_id: "run_1", job_id: "job_123", status: "stopped" });
+ }),
+ http.post("https://async.api.zenrows.com/v1/jobs/job_123/rerun", () => {
+ return HttpResponse.json(
+ { job_id: "job_123", status: "open", latest_run: { run_id: "run_2" }, rerun_of: "run_1" },
+ { status: 201 },
+ );
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/runs", () => {
+ return HttpResponse.json({ runs: [{ run_id: "run_1" }], next_cursor: null });
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/runs/run_1", () => {
+ return HttpResponse.json({ run_id: "run_1", job_id: "job_123", status: "completed" });
+ }),
+ http.delete("https://async.api.zenrows.com/v1/jobs/job_123/runs/run_1", () => {
+ return new HttpResponse(null, { status: 202 });
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/runs/run_1/results", () => {
+ return HttpResponse.json({ results: [{ task_id: "task_1" }], next_cursor: null });
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/tasks/task_1/content", () => {
+ return HttpResponse.text("latest run content");
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/runs/run_1/tasks/task_1/content", () => {
+ return HttpResponse.text("run_1 content");
+ }),
+ // Error fixtures — each job id encodes the failure it triggers, so tests can hit them by name.
+ http.post("https://async.api.zenrows.com/v1/jobs/job_no_credit/tasks", () => {
+ return HttpResponse.json(
+ {
+ type: "about:blank",
+ title: "Payment Required",
+ status: 402,
+ detail: "No credit available",
+ },
+ { status: 402 },
+ );
+ }),
+ http.post("https://async.api.zenrows.com/v1/jobs/job_conflict/close", () => {
+ return HttpResponse.json(
+ { type: "about:blank", title: "Conflict", status: 409, detail: "Job is not open" },
+ { status: 409 },
+ );
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_unauthorized", () => {
+ return HttpResponse.json(
+ {
+ type: "about:blank",
+ title: "Unauthorized",
+ status: 401,
+ detail: "Missing / invalid API key",
+ },
+ { status: 401 },
+ );
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_upstream_down", () => {
+ return HttpResponse.json(
+ {
+ type: "about:blank",
+ title: "Service Unavailable",
+ status: 503,
+ detail: "Transient upstream failure",
+ },
+ { status: 503 },
+ );
+ }),
+ // Simulates an upstream failure mode that doesn't even return valid JSON on error.
+ http.get("https://async.api.zenrows.com/v1/jobs/job_broken_upstream", () => {
+ return new HttpResponse("502 Bad Gateway", {
+ status: 502,
+ headers: { "Content-Type": "text/html" },
+ });
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/tasks/task_failed/content", () => {
+ return HttpResponse.json(
+ { type: "about:blank", title: "Unprocessable Entity", status: 422, detail: "Task failed" },
+ { status: 422 },
+ );
+ }),
+
+ // ----- schedule -----
+ http.put("https://async.api.zenrows.com/v1/jobs/job_sched/schedule", () => {
+ return HttpResponse.json({ job_id: "job_sched", status: "closed", type: "scheduled" });
+ }),
+ http.post("https://async.api.zenrows.com/v1/jobs/job_sched/schedule/state", () => {
+ return HttpResponse.json({ job_id: "job_sched", status: "closed", schedule_state: "paused" });
+ }),
+ http.post("https://async.api.zenrows.com/v1/jobs/job_123/pause", () => {
+ return HttpResponse.json({ run_id: "run_1", job_id: "job_123", pause_state: "paused" });
+ }),
+ http.post("https://async.api.zenrows.com/v1/jobs/job_123/resume", () => {
+ return HttpResponse.json({ run_id: "run_1", job_id: "job_123", pause_state: "active" });
+ }),
+
+ // ----- webhooks -----
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/webhook", () => {
+ return HttpResponse.json({ url: "https://example.com/hook", signature: true });
+ }),
+ http.put("https://async.api.zenrows.com/v1/jobs/job_123/webhook", () => {
+ return HttpResponse.json({ url: "https://example.com/hook2", signature: false });
+ }),
+ http.delete("https://async.api.zenrows.com/v1/jobs/job_123/webhook", () => {
+ return new HttpResponse(null, { status: 204 });
+ }),
+ http.post("https://async.api.zenrows.com/v1/webhook/test", () => {
+ return HttpResponse.json({
+ delivered: true,
+ event_id: "evt_1",
+ status_code: 200,
+ elapsed_ms: 42,
+ });
+ }),
+
+ // ----- HMAC key lifecycle -----
+ http.get("https://async.api.zenrows.com/v1/hmac/keys", () => {
+ return HttpResponse.json({
+ active: { kid: "01AAAAAAAAAAAAAAAAAAAAAAAA", created_at: "2026-01-01T00:00:00Z" },
+ });
+ }),
+ http.post("https://async.api.zenrows.com/v1/hmac/keys/rotate", () => {
+ return HttpResponse.json({
+ kid: "01BBBBBBBBBBBBBBBBBBBBBBBB",
+ secret: "c2VjcmV0",
+ created_at: "2026-01-02T00:00:00Z",
+ });
+ }),
+ http.delete("https://async.api.zenrows.com/v1/hmac/keys/rotate", () => {
+ return new HttpResponse(null, { status: 204 });
+ }),
+ http.post("https://async.api.zenrows.com/v1/hmac/keys/rotate/finalize", () => {
+ return HttpResponse.json({
+ active_kid: "01BBBBBBBBBBBBBBBBBBBBBBBB",
+ created_at: "2026-01-02T00:00:00Z",
+ });
+ }),
+
+ // ----- CSV upload (job_inputs) -----
+ http.post("https://async.api.zenrows.com/v1/job_inputs", () => {
+ return HttpResponse.json({
+ file_input_id: "file_123",
+ upload: {
+ method: "PUT",
+ url: "https://storage.example.test/presigned-upload",
+ headers: { "Content-Type": "text/csv" },
+ expires_at: "2026-01-01T01:00:00Z",
+ },
+ expires_at: "2026-01-02T00:00:00Z",
+ });
+ }),
+ http.put("https://storage.example.test/presigned-upload", () => {
+ return new HttpResponse(null, { status: 200 });
+ }),
+
+ // ----- results export -----
+ http.post("https://async.api.zenrows.com/v1/jobs/job_123/runs/run_1/exports", () => {
+ return HttpResponse.json(
+ {
+ export_id: "01EXPORTAAAAAAAAAAAAAAAAAA",
+ status: "pending",
+ created_at: "2026-01-01T00:00:00Z",
+ expires_at: "2026-01-01T12:00:00Z",
+ },
+ { status: 201 },
+ );
+ }),
+ http.get(
+ "https://async.api.zenrows.com/v1/jobs/job_123/runs/run_1/exports/01EXPORTAAAAAAAAAAAAAAAAAA",
+ () => {
+ return HttpResponse.json({
+ export_id: "01EXPORTAAAAAAAAAAAAAAAAAA",
+ status: "completed",
+ download_url: "https://storage.example.test/export.zip",
+ created_at: "2026-01-01T00:00:00Z",
+ expires_at: "2026-01-01T12:00:00Z",
+ });
+ },
+ ),
+ http.get("https://storage.example.test/export.zip", () => {
+ return new HttpResponse(new Blob([new Uint8Array([1, 2, 3, 4])]), { status: 200 });
+ }),
+
+ // ----- task history -----
+ http.get("https://async.api.zenrows.com/v1/jobs/job_123/tasks/task_1/history", () => {
+ return HttpResponse.json({
+ events: [
+ { started_at: "2026-01-01T00:00:00Z", ended_at: "2026-01-01T00:00:01Z", attempt: 1 },
+ ],
+ });
+ }),
+
+ // ----- retry: fails twice with 503, succeeds on the 3rd attempt -----
+ http.get(
+ "https://async.api.zenrows.com/v1/jobs/job_retry_then_ok",
+ (() => {
+ let calls = 0;
+ return () => {
+ calls += 1;
+ if (calls < 3) {
+ return HttpResponse.json(
+ { type: "about:blank", title: "Service Unavailable", status: 503 },
+ { status: 503 },
+ );
+ }
+ return HttpResponse.json({ job_id: "job_retry_then_ok", status: "open" });
+ };
+ })(),
+ ),
+ // ----- retry: honors Retry-After -----
+ http.get(
+ "https://async.api.zenrows.com/v1/jobs/job_retry_after",
+ (() => {
+ let calls = 0;
+ return () => {
+ calls += 1;
+ if (calls === 1) {
+ return HttpResponse.json(
+ { type: "about:blank", title: "Too Many Requests", status: 429 },
+ { status: 429, headers: { "Retry-After": "0" } },
+ );
+ }
+ return HttpResponse.json({ job_id: "job_retry_after", status: "open" });
+ };
+ })(),
+ ),
+ // ----- download: a job whose results carry real presigned result_urls -----
+ http.get("https://async.api.zenrows.com/v1/jobs/job_download/results", () => {
+ return HttpResponse.json({
+ results: [
+ {
+ task_id: "task_a",
+ external_id: "ext_a",
+ run_id: "run_dl",
+ url: "https://example.com/a",
+ status: "successful",
+ result_url: "https://storage.example.test/body-a",
+ },
+ {
+ task_id: "task_b",
+ run_id: "run_dl",
+ url: "https://example.com/b",
+ status: "successful",
+ result_url: "https://storage.example.test/body-b",
+ },
+ ],
+ next_cursor: null,
+ });
+ }),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_download/runs/run_dl/results", () => {
+ return HttpResponse.json({
+ results: [
+ {
+ task_id: "task_a",
+ external_id: "ext_a",
+ run_id: "run_dl",
+ url: "https://example.com/a",
+ status: "successful",
+ result_url: "https://storage.example.test/body-a",
+ },
+ {
+ task_id: "task_b",
+ run_id: "run_dl",
+ url: "https://example.com/b",
+ status: "successful",
+ result_url: "https://storage.example.test/body-b",
+ },
+ ],
+ next_cursor: null,
+ });
+ }),
+ http.get(
+ "https://storage.example.test/body-a",
+ () => new HttpResponse("body A", { status: 200 }),
+ ),
+ http.get(
+ "https://storage.example.test/body-b",
+ () => new HttpResponse("body B", { status: 200 }),
+ ),
+
+ // ----- job whose ingest is pending on the 1st poll, done on the 2nd -----
+ http.get(
+ "https://async.api.zenrows.com/v1/jobs/job_ingest_pending",
+ (() => {
+ let calls = 0;
+ return () => {
+ calls += 1;
+ return HttpResponse.json({
+ job_id: "job_ingest_pending",
+ status: "open",
+ latest_run: {
+ run_id: "run_ip",
+ job_id: "job_ingest_pending",
+ status: "running",
+ ingest_status: calls < 2 ? "pending" : "done",
+ },
+ });
+ };
+ })(),
+ ),
+ http.get("https://async.api.zenrows.com/v1/jobs/job_no_run", () => {
+ return HttpResponse.json({ job_id: "job_no_run", status: "closed" });
+ }),
+
+ // ----- retry: a network-level failure (not an HTTP status) on a GET, then success -----
+ http.get(
+ "https://async.api.zenrows.com/v1/jobs/job_network_blip",
+ (() => {
+ let calls = 0;
+ return () => {
+ calls += 1;
+ if (calls === 1) {
+ return HttpResponse.error();
+ }
+ return HttpResponse.json({ job_id: "job_network_blip", status: "open" });
+ };
+ })(),
+ ),
+
+ // ----- retry: POST without an Idempotency-Key is never retried on 503 -----
+ http.post(
+ "https://async.api.zenrows.com/v1/jobs/job_no_retry_post/tasks",
+ (() => {
+ let calls = 0;
+ return () => {
+ calls += 1;
+ return HttpResponse.json(
+ { type: "about:blank", title: "Service Unavailable", status: 503 },
+ { status: 503 },
+ );
+ };
+ })(),
+ ),
];
export const server = setupServer(...handlers);
diff --git a/tests/batch-client.test.ts b/tests/batch-client.test.ts
new file mode 100644
index 0000000..a505e2c
--- /dev/null
+++ b/tests/batch-client.test.ts
@@ -0,0 +1,257 @@
+import { beforeEach, describe, expect, test } from "vitest";
+import { ZenRowsBatchClient } from "../src/batch/client";
+import { ZenRowsBatchError } from "../src/batch/errors";
+import "./_setup";
+
+describe("ZenRowsBatchClient — retry/backoff transport", () => {
+ let client: ZenRowsBatchClient;
+
+ beforeEach(() => {
+ client = new ZenRowsBatchClient("API_KEY");
+ });
+
+ test("retries a GET on 503 and eventually succeeds", async () => {
+ const job = await client.getJob("job_retry_then_ok");
+ expect(job.data).toMatchObject({ job_id: "job_retry_then_ok" });
+ });
+
+ test("honors Retry-After on a 429", async () => {
+ const job = await client.getJob("job_retry_after");
+ expect(job.data).toMatchObject({ job_id: "job_retry_after" });
+ });
+
+ test("retries a GET on a network-level failure (not an HTTP status)", async () => {
+ const job = await client.getJob("job_network_blip");
+ expect(job.data).toMatchObject({ job_id: "job_network_blip" });
+ });
+
+ test("does not retry a POST without an Idempotency-Key, even on a retryable status", async () => {
+ await expect(
+ client.job("job_no_retry_post").addTasks([{ url: "https://example.com" }]),
+ ).rejects.toThrow(ZenRowsBatchError);
+ });
+});
+
+describe("ZenRowsBatchClient — error shape", () => {
+ let client: ZenRowsBatchClient;
+
+ beforeEach(() => {
+ client = new ZenRowsBatchClient("API_KEY");
+ });
+
+ test("carries status, code, and problem detail from a Problem+JSON body", async () => {
+ await expect(
+ client.job("job_no_credit").addTasks([{ url: "https://example.com" }]),
+ ).rejects.toSatisfy((error: unknown) => {
+ expect(error).toBeInstanceOf(ZenRowsBatchError);
+ const err = error as ZenRowsBatchError;
+ expect(err.status).toBe(402);
+ expect(err.problem?.title).toBe("Payment Required");
+ return true;
+ });
+ });
+
+ test("degrades gracefully on a non-JSON error body, still carrying the real status", async () => {
+ await expect(client.getJob("job_broken_upstream")).rejects.toSatisfy((error: unknown) => {
+ const err = error as ZenRowsBatchError;
+ expect(err.status).toBe(502);
+ expect(err.problem).toBeUndefined();
+ expect(err.code).toBe("internal");
+ return true;
+ });
+ });
+});
+
+describe("ZenRowsBatchClient — submit* validation", () => {
+ let client: ZenRowsBatchClient;
+
+ beforeEach(() => {
+ client = new ZenRowsBatchClient("API_KEY");
+ });
+
+ test("submitRegular rejects passing both urls and fileInputId", async () => {
+ await expect(client.submitRegular(["https://a.com"], "file_1")).rejects.toThrow(
+ /urls OR fileInputId/,
+ );
+ });
+
+ test("submitRegular rejects passing neither urls nor fileInputId", async () => {
+ await expect(client.submitRegular()).rejects.toThrow(/require urls or fileInputId/);
+ });
+
+ test("submitOpen allows starting with no tasks at all", async () => {
+ const ref = await client.submitOpen();
+ expect(ref.jobId).toBe("job_123");
+ });
+
+ test("submitScheduled rejects passing both urls and fileInputId", async () => {
+ const { Rate } = await import("../src/batch/schedule");
+ await expect(
+ client.submitScheduled(new Rate(15, "minute"), ["https://a.com"], "file_1"),
+ ).rejects.toThrow(/urls OR fileInputId/);
+ });
+});
+
+describe("ZenRowsBatchClient — webhooks", () => {
+ let client: ZenRowsBatchClient;
+
+ beforeEach(() => {
+ client = new ZenRowsBatchClient("API_KEY");
+ });
+
+ test("gets, replaces, and deletes a job's webhook config", async () => {
+ const current = await client.getJobWebhook("job_123");
+ expect(current).toMatchObject({ url: "https://example.com/hook", signature: true });
+
+ const replaced = await client.putJobWebhook("job_123", { url: "https://example.com/hook2" });
+ expect(replaced).toMatchObject({ url: "https://example.com/hook2", signature: false });
+
+ await expect(client.deleteJobWebhook("job_123")).resolves.toBeUndefined();
+ });
+
+ test("dispatches a synthetic test event", async () => {
+ const result = await client.testWebhook({ url: "https://example.com/hook" });
+ expect(result).toMatchObject({ delivered: true, status_code: 200 });
+ });
+});
+
+describe("ZenRowsBatchClient — HMAC key lifecycle", () => {
+ let client: ZenRowsBatchClient;
+
+ beforeEach(() => {
+ client = new ZenRowsBatchClient("API_KEY");
+ });
+
+ test("lists, rotates, finalizes, and cancels rotation", async () => {
+ const list = await client.listHmacKeys();
+ expect(list.active?.kid).toBe("01AAAAAAAAAAAAAAAAAAAAAAAA");
+
+ const created = await client.rotateHmacKey();
+ expect(created.secret).toBe("c2VjcmV0");
+
+ const finalized = await client.finalizeHmacKey();
+ expect(finalized.active_kid).toBe("01BBBBBBBBBBBBBBBBBBBBBBBB");
+
+ await expect(client.cancelHmacRotation()).resolves.toBeUndefined();
+ });
+});
+
+describe("ZenRowsBatchClient — CSV upload", () => {
+ let client: ZenRowsBatchClient;
+
+ beforeEach(() => {
+ client = new ZenRowsBatchClient("API_KEY");
+ });
+
+ test("allocates a slot then PUTs the body to the presigned URL, returning file_input_id", async () => {
+ const fileInputId = await client.uploadCsv("url\nhttps://a.com\n", {
+ fields: { url: 0 },
+ header: true,
+ });
+ expect(fileInputId).toBe("file_123");
+ });
+});
+
+describe("ZenRowsBatchClient — task history", () => {
+ test("returns the attempt history for a task", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const history = await client.getTaskHistory("job_123", "task_1");
+ expect(history.events).toHaveLength(1);
+ expect(history.events[0]).toMatchObject({ attempt: 1 });
+ });
+});
+
+describe("ZenRowsBatchClient — results export", () => {
+ test("starts an export, waits for completion, and reports the download URL", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const exportRef = await client.startResultsExport("job_123", "run_1");
+ expect(exportRef.exportId).toBe("01EXPORTAAAAAAAAAAAAAAAAAA");
+
+ const final = await client.waitForExport("job_123", "run_1", exportRef.exportId, {
+ pollInterval: 0.01,
+ });
+ expect(final).toMatchObject({
+ status: "completed",
+ download_url: "https://storage.example.test/export.zip",
+ });
+ });
+});
+
+describe("ZenRowsBatchClient — waitForRun / downloadAllResults", () => {
+ test("waitForRun resolves once the run reaches a terminal status", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const run = await client.waitForRun("job_123", { runId: "run_1", pollInterval: 0.01 });
+ expect(run.status).toBe("completed");
+ });
+
+ test("downloadAllResults starts an export, waits, and streams the zip to disk", async () => {
+ const { mkdtemp, readFile, rm } = await import("node:fs/promises");
+ const { tmpdir } = await import("node:os");
+ const { join } = await import("node:path");
+ const client = new ZenRowsBatchClient("API_KEY");
+ const dir = await mkdtemp(join(tmpdir(), "zenrows-batch-all-"));
+ try {
+ const target = join(dir, "all.zip");
+ const written = await client.downloadAllResults("job_123", "run_1", target, {
+ pollInterval: 0.01,
+ });
+ expect(written).toBe(target);
+ expect((await readFile(target)).length).toBeGreaterThan(0);
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ });
+});
+
+describe("ZenRowsBatchClient — error parsing edge cases", () => {
+ test("parseProblem tolerates a non-object JSON body (e.g. a bare array or string)", async () => {
+ const { parseProblem } = await import("../src/batch/errors");
+ const response = new Response(JSON.stringify(["not", "an", "object"]), { status: 500 });
+ const { problem, extras } = await parseProblem(response);
+ expect(problem).toBeUndefined();
+ expect(extras).toBeUndefined();
+ });
+
+ test("parseProblem preserves non-standard fields as extras", async () => {
+ const { parseProblem } = await import("../src/batch/errors");
+ const response = new Response(
+ JSON.stringify({
+ type: "about:blank",
+ title: "Bad",
+ status: 400,
+ code: "invalid_tasks",
+ invalid_tasks: [1, 2],
+ }),
+ { status: 400 },
+ );
+ const { problem, extras } = await parseProblem(response);
+ expect(problem?.code).toBe("invalid_tasks");
+ expect(extras).toEqual({ invalid_tasks: [1, 2] });
+ });
+});
+
+describe("ZenRowsBatchClient — scheduling", () => {
+ test("replaces a job's schedule and pauses/resumes it", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const job = client.job("job_sched");
+ const { Rate } = await import("../src/batch/schedule");
+ const updated = await job.schedule.update(new Rate(15, "minute"));
+ expect(updated.data.job_id).toBe("job_sched");
+
+ const paused = await job.schedule.pause();
+ expect(paused.data.schedule_state).toBe("paused");
+ });
+});
+
+describe("ZenRowsBatchClient — current-run pause/resume", () => {
+ test("pauses and resumes the current run", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const job = client.job("job_123");
+
+ const paused = await job.run.pause();
+ expect(paused.data.pause_state).toBe("paused");
+
+ const resumed = await job.run.resume();
+ expect(resumed.data.pause_state).toBe("active");
+ });
+});
diff --git a/tests/batch-download.test.ts b/tests/batch-download.test.ts
new file mode 100644
index 0000000..2fb5e46
--- /dev/null
+++ b/tests/batch-download.test.ts
@@ -0,0 +1,72 @@
+import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { ZenRowsBatchClient } from "../src/batch/client";
+import "./_setup";
+
+describe("downloadToDir / downloadToMemory / single-task download", () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ let dir: string;
+
+ beforeEach(async () => {
+ dir = await mkdtemp(join(tmpdir(), "zenrows-batch-test-"));
+ });
+
+ afterEach(async () => {
+ await rm(dir, { recursive: true, force: true });
+ });
+
+ test("downloadToDir writes one file per successful task, named by task_id by default", async () => {
+ const count = await client.downloadToDir("job_download", undefined, dir);
+ expect(count).toBe(2);
+ const files = (await readdir(dir)).sort();
+ expect(files).toEqual(["task_a", "task_b"]);
+ expect(await readFile(join(dir, "task_a"), "utf-8")).toBe("body A");
+ });
+
+ test("downloadToDir can name files by external_id when requested", async () => {
+ await client.downloadToDir("job_download", undefined, dir, { useExternalId: true });
+ const files = (await readdir(dir)).sort();
+ // task_b has no external_id, so it still falls back to task_id.
+ expect(files).toEqual(["ext_a", "task_b"]);
+ });
+
+ test("downloadToDir honors a custom nameFn", async () => {
+ await client.downloadToDir("job_download", undefined, dir, {
+ nameFn: (task) => `${task.task_id}.html`,
+ });
+ const files = (await readdir(dir)).sort();
+ expect(files).toEqual(["task_a.html", "task_b.html"]);
+ });
+
+ test("downloadToMemory returns every body as a DownloadedResult", async () => {
+ const results = await client.downloadToMemory("job_download", undefined);
+ expect(results).toHaveLength(2);
+ const byTaskId = Object.fromEntries(results.map((r) => [r.taskId, r.body.toString("utf-8")]));
+ expect(byTaskId.task_a).toBe("body A");
+ expect(byTaskId.task_b).toBe("body B");
+ });
+
+ test("downloadTaskToFile / downloadTaskToMemory work on a single already-held TaskResult", async () => {
+ const { results } = await client.getResults("job_download");
+ const task = results[0];
+ const memory = await client.downloadTaskToMemory(task);
+ expect(memory.toString("utf-8")).toBe("body A");
+
+ const target = join(dir, "single.html");
+ await client.downloadTaskToFile(task, target);
+ expect(await readFile(target, "utf-8")).toBe("body A");
+ });
+
+ test("downloadTaskToMemory throws a clear error for a task with no result_url", async () => {
+ await expect(
+ client.downloadTaskToMemory({
+ task_id: "no_url",
+ run_id: "r",
+ url: "https://x.com",
+ status: "failed",
+ }),
+ ).rejects.toThrow(/no result_url/);
+ });
+});
diff --git a/tests/batch-estimate.test.ts b/tests/batch-estimate.test.ts
new file mode 100644
index 0000000..30d03ae
--- /dev/null
+++ b/tests/batch-estimate.test.ts
@@ -0,0 +1,78 @@
+import { describe, expect, test } from "vitest";
+import { estimateCost } from "../src/batch/estimate";
+
+describe("estimateCost", () => {
+ test("bare URL strings default to the base tier", () => {
+ const result = estimateCost(["https://a.com", "https://b.com"]);
+ expect(result).toMatchObject({ taskCount: 2, min: 2, max: 2, exact: true });
+ expect(result.breakdown).toEqual([{ tier: "base", count: 2, unitMin: 1, unitMax: 1 }]);
+ });
+
+ test("js_render alone prices at the JS tier", () => {
+ const result = estimateCost([{ zenrows_params: { js_render: true } }]);
+ expect(result).toMatchObject({ min: 5, max: 5, exact: true });
+ });
+
+ test("premium_proxy alone prices at the premium tier", () => {
+ const result = estimateCost([{ zenrows_params: { premium_proxy: true } }]);
+ expect(result).toMatchObject({ min: 10, max: 10, exact: true });
+ });
+
+ test("js_render + premium_proxy together price at the combined tier", () => {
+ const result = estimateCost([{ zenrows_params: { js_render: true, premium_proxy: true } }]);
+ expect(result).toMatchObject({ min: 25, max: 25, exact: true });
+ });
+
+ test("mode=auto is a [1, 25] range and makes the estimate inexact", () => {
+ const result = estimateCost([{ zenrows_params: { mode: "auto" } }]);
+ expect(result).toMatchObject({ min: 1, max: 25, exact: false });
+ });
+
+ test("mode=auto wins even if js_render/premium_proxy are also set (malformed input)", () => {
+ const result = estimateCost([{ zenrows_params: { mode: "auto", js_render: true } }]);
+ expect(result.breakdown[0]?.tier).toBe("auto");
+ });
+
+ test("truthy string spellings ('1', 'yes', 'ON') count as on", () => {
+ expect(estimateCost([{ zenrows_params: { js_render: "yes" } }]).min).toBe(5);
+ expect(estimateCost([{ zenrows_params: { premium_proxy: "1" } }]).min).toBe(10);
+ expect(estimateCost([{ zenrows_params: { js_render: "ON" } }]).min).toBe(5);
+ });
+
+ test("falsy string spellings and zero do not count as on", () => {
+ const result = estimateCost([{ zenrows_params: { js_render: "false", premium_proxy: 0 } }]);
+ expect(result.min).toBe(1);
+ });
+
+ test("per-task params override job-level params on key collision (task wins)", () => {
+ const result = estimateCost([{ zenrows_params: { js_render: false } }], { js_render: true });
+ // job-level says js_render:true, task overrides to false -> base tier
+ expect(result.min).toBe(1);
+ });
+
+ test("job-level params apply when a task has no override", () => {
+ const result = estimateCost(["https://a.com"], { premium_proxy: true });
+ expect(result.min).toBe(10);
+ });
+
+ test("breakdown aggregates counts per tier and renders in stable order", () => {
+ const result = estimateCost([
+ "https://a.com",
+ "https://b.com",
+ { zenrows_params: { js_render: true } },
+ { zenrows_params: { mode: "auto" } },
+ ]);
+ expect(result.breakdown).toEqual([
+ { tier: "base", count: 2, unitMin: 1, unitMax: 1 },
+ { tier: "js_render", count: 1, unitMin: 5, unitMax: 5 },
+ { tier: "auto", count: 1, unitMin: 1, unitMax: 25 },
+ ]);
+ expect(result.min).toBe(2 * 1 + 5 + 1);
+ expect(result.max).toBe(2 * 1 + 5 + 25);
+ });
+
+ test("empty task list estimates to zero, exact", () => {
+ const result = estimateCost([]);
+ expect(result).toMatchObject({ taskCount: 0, min: 0, max: 0, exact: true, breakdown: [] });
+ });
+});
diff --git a/tests/batch-resources.test.ts b/tests/batch-resources.test.ts
new file mode 100644
index 0000000..88049b9
--- /dev/null
+++ b/tests/batch-resources.test.ts
@@ -0,0 +1,248 @@
+import { describe, expect, test } from "vitest";
+import { ZenRowsBatchClient } from "../src/batch/client";
+import "./_setup";
+
+describe("JobRef / JobHandle", () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+
+ test("job() mints a ref with no network call", () => {
+ const ref = client.job("job_123");
+ expect(ref.jobId).toBe("job_123");
+ expect(ref.status).toBeUndefined();
+ });
+
+ test("submitJob returns a ref carrying the submit response's status/acceptedTasks", async () => {
+ const ref = await client.submitJob({ tasks: [{ url: "https://example.com" }] });
+ expect(ref.status).toBe("open");
+ expect(ref.acceptedTasks).toBeUndefined(); // fixture doesn't set accepted_tasks
+ });
+
+ test("load() fetches and returns a JobHandle with .data populated", async () => {
+ const handle = await client.job("job_123").load();
+ expect(handle.data).toMatchObject({ job_id: "job_123" });
+ expect(handle.status).toBe(handle.data.status);
+ });
+
+ test("close() returns a FRESH handle with the server's new state, not a mutated original", async () => {
+ const ref = client.job("job_123");
+ const closed = await ref.close();
+ expect(closed.data.status).toBe("closed");
+ // The original ref is untouched — it's not a JobHandle and has no .data.
+ expect((ref as unknown as { data?: unknown }).data).toBeUndefined();
+ });
+
+ test("delete() resolves with no return value", async () => {
+ await expect(client.job("job_123").delete()).resolves.toBeUndefined();
+ });
+
+ test("addTasks forwards lastBatch through to the request body", async () => {
+ const result = await client.job("job_123").addTasks([{ url: "https://example.com/3" }], {
+ lastBatch: true,
+ });
+ expect(result.accepted_tasks).toBe(1);
+ });
+
+ test("rerun() with no status returns a RunHandle for the new run", async () => {
+ const run = await client.job("job_123").rerun();
+ expect(run.data.run_id).toBe("run_2");
+ });
+
+ test("retryFailed() is a shortcut for rerun({ status: 'failed' })", async () => {
+ const run = await client.job("job_123").retryFailed();
+ expect(run.data.run_id).toBe("run_2");
+ });
+
+ test("run facet is lazily created and memoised (same instance across accesses)", () => {
+ const ref = client.job("job_123");
+ expect(ref.run).toBe(ref.run);
+ });
+
+ test("schedule facet is lazily created and memoised", () => {
+ const ref = client.job("job_sched");
+ expect(ref.schedule).toBe(ref.schedule);
+ });
+
+ test("getWebhook/setWebhook/deleteWebhook delegate to the client", async () => {
+ const ref = client.job("job_123");
+ const config = await ref.getWebhook();
+ expect(config.url).toBe("https://example.com/hook");
+ const replaced = await ref.setWebhook("https://example.com/hook2", false);
+ expect(replaced.signature).toBe(false);
+ await expect(ref.deleteWebhook()).resolves.toBeUndefined();
+ });
+
+ test("retryFailed(includePending: true) sends a combined status filter", async () => {
+ const run = await client.job("job_123").retryFailed({ includePending: true });
+ expect(run.data.run_id).toBe("run_2");
+ });
+
+ test("waitForIngest resolves once ingest_status leaves pending, returning a loaded JobHandle", async () => {
+ const handle = await client.job("job_ingest_pending").waitForIngest({ pollInterval: 0.01 });
+ expect(handle.data.latest_run?.ingest_status).toBe("done");
+ });
+
+ test("addFileInput uploads a CSV and returns the file_input_id", async () => {
+ const fileInputId = await client.job("job_123").addFileInput("url\nhttps://a.com\n", {
+ fields: { url: 0 },
+ header: true,
+ });
+ expect(fileInputId).toBe("file_123");
+ });
+});
+
+describe("ScheduleControls.resume", () => {
+ test("re-enables scheduled fires on a paused job", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const resumed = await client.job("job_sched").schedule.resume();
+ expect(resumed.data.job_id).toBe("job_sched");
+ });
+});
+
+describe("submitJob(waitForIngest: true)", () => {
+ test("returns a loaded JobHandle instead of a bare ref when ingestion was actually polled", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ // submitJob's own fixture (job_123) has no latest_run, so waitForIngest is a no-op here —
+ // this exercises the branch where wait is requested but there's nothing to wait for.
+ const ref = await client.submitJob({
+ tasks: [{ url: "https://example.com" }],
+ waitForIngest: true,
+ });
+ expect(ref.jobId).toBe("job_123");
+ });
+});
+
+describe("RunRef / RunHandle", () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+
+ test("run() mints a ref with no network call", () => {
+ const ref = client.run("job_123", "run_1");
+ expect(ref.jobId).toBe("job_123");
+ expect(ref.runId).toBe("run_1");
+ });
+
+ test("load() returns a RunHandle exposing .status and .stats shortcuts", async () => {
+ const handle = await client.run("job_123", "run_1").load();
+ expect(handle.status).toBe(handle.data.status);
+ });
+
+ test("delete() scrubs a single run", async () => {
+ await expect(client.run("job_123", "run_1").delete()).resolves.toBeUndefined();
+ });
+
+ test("export(id) mints an ExportRef with no network call", () => {
+ const ref = client.run("job_123", "run_1").export("01EXPORTAAAAAAAAAAAAAAAAAA");
+ expect(ref.exportId).toBe("01EXPORTAAAAAAAAAAAAAAAAAA");
+ });
+
+ test("startExport() kicks off an async export", async () => {
+ const ref = await client.run("job_123", "run_1").startExport();
+ expect(ref.exportId).toBe("01EXPORTAAAAAAAAAAAAAAAAAA");
+ });
+
+ test("wait() resolves once the run reaches a terminal status", async () => {
+ const handle = await client.run("job_123", "run_1").wait({ pollInterval: 0.01 });
+ expect(handle.status).toBe("completed");
+ });
+
+ test("downloadToDir / downloadToMemory delegate through to the client's download helpers", async () => {
+ const { mkdtemp, rm } = await import("node:fs/promises");
+ const { tmpdir } = await import("node:os");
+ const { join } = await import("node:path");
+ const dir = await mkdtemp(join(tmpdir(), "zenrows-run-dl-"));
+ try {
+ const count = await client.run("job_download", "run_dl").downloadToDir(dir);
+ expect(count).toBe(2);
+ const inMemory = await client.run("job_download", "run_dl").downloadToMemory();
+ expect(inMemory).toHaveLength(2);
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ });
+});
+
+describe("ExportRef / ExportHandle", () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+
+ test("load() fetches the export and exposes .status", async () => {
+ const handle = await client.run("job_123", "run_1").export("01EXPORTAAAAAAAAAAAAAAAAAA").load();
+ expect(handle.status).toBe("completed");
+ });
+
+ test("wait() resolves once the export is completed", async () => {
+ const handle = await client
+ .run("job_123", "run_1")
+ .export("01EXPORTAAAAAAAAAAAAAAAAAA")
+ .wait({ pollInterval: 0.01 });
+ expect(handle.status).toBe("completed");
+ });
+
+ test("downloadToPath streams the completed export's zip to disk", async () => {
+ const { mkdtemp, readFile, rm } = await import("node:fs/promises");
+ const { tmpdir } = await import("node:os");
+ const { join } = await import("node:path");
+ const dir = await mkdtemp(join(tmpdir(), "zenrows-export-dl-"));
+ try {
+ const target = join(dir, "export.zip");
+ await client
+ .run("job_123", "run_1")
+ .export("01EXPORTAAAAAAAAAAAAAAAAAA")
+ .downloadToPath(target);
+ const bytes = await readFile(target);
+ expect(bytes.length).toBeGreaterThan(0);
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ });
+});
+
+describe("current run facet — job.run", () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+
+ test("stop() returns a fresh RunHandle", async () => {
+ const handle = await client.job("job_123").run.stop();
+ expect(handle.status).toBe("stopped");
+ });
+
+ test("cancel() is an alias for stop()", async () => {
+ const handle = await client.job("job_123").run.cancel();
+ expect(handle.status).toBe("stopped");
+ });
+});
+
+describe("pagination iterators", () => {
+ test("iterJobs stops when next_cursor is absent (empty page)", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const jobs = [];
+ for await (const job of client.iterJobs()) {
+ jobs.push(job);
+ }
+ expect(jobs).toEqual([]);
+ });
+
+ test("iterRuns yields every run on the page", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const runs = [];
+ for await (const run of client.iterRuns("job_123")) {
+ runs.push(run.data.run_id);
+ }
+ expect(runs).toEqual(["run_1"]);
+ });
+
+ test("iterResults yields every result on the page", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const results = [];
+ for await (const result of client.iterResults("job_123")) {
+ results.push(result.task_id);
+ }
+ expect(results).toEqual([]);
+ });
+
+ test("JobRef.runs() delegates to the client's iterRuns", async () => {
+ const client = new ZenRowsBatchClient("API_KEY");
+ const runs = [];
+ for await (const run of client.job("job_123").runs()) {
+ runs.push(run.data.run_id);
+ }
+ expect(runs).toEqual(["run_1"]);
+ });
+});
diff --git a/tests/batch-schedule.test.ts b/tests/batch-schedule.test.ts
new file mode 100644
index 0000000..073befd
--- /dev/null
+++ b/tests/batch-schedule.test.ts
@@ -0,0 +1,115 @@
+import { describe, expect, test } from "vitest";
+import { At, Calendar, Daily, Monthly, Rate, Weekly } from "../src/batch/schedule";
+
+describe("At", () => {
+ test("accepts a tz-naive ISO string", () => {
+ const at = new At("2026-09-01T09:00:00", "Europe/Berlin");
+ expect(at.toRequestBody()).toEqual({ at: "2026-09-01T09:00:00", timezone: "Europe/Berlin" });
+ });
+
+ test("rejects a string with a Z suffix", () => {
+ expect(() => new At("2026-09-01T09:00:00Z", "Europe/Berlin")).toThrow(/tz-naive/);
+ });
+
+ test("rejects a string with a numeric offset suffix", () => {
+ expect(() => new At("2026-09-01T09:00:00+02:00", "Europe/Berlin")).toThrow(/tz-naive/);
+ });
+
+ test("rejects an empty string", () => {
+ expect(() => new At("", "Europe/Berlin")).toThrow(/non-empty/);
+ });
+
+ test("rejects an invalid IANA timezone", () => {
+ expect(() => new At("2026-09-01T09:00:00", "Not/AZone")).toThrow(/not a valid IANA timezone/);
+ });
+
+ test("rejects an empty timezone", () => {
+ expect(() => new At("2026-09-01T09:00:00", "")).toThrow(/is required/);
+ });
+
+ test("formats a Date using its local wall-clock fields", () => {
+ const date = new Date(2026, 8, 1, 9, 0, 0); // month is 0-indexed: September
+ const at = new At(date, "Europe/Berlin");
+ expect(at.at).toBe("2026-09-01T09:00:00");
+ });
+});
+
+describe("Rate", () => {
+ test("builds the wire body", () => {
+ expect(new Rate(15, "minute").toRequestBody()).toEqual({ rate: { every: 15, unit: "minute" } });
+ });
+
+ test("rejects every < 1", () => {
+ expect(() => new Rate(0, "minute")).toThrow(/>= 1/);
+ });
+
+ test("rejects a non-integer every", () => {
+ expect(() => new Rate(1.5, "minute")).toThrow(/integer/);
+ });
+
+ test("rejects an invalid unit", () => {
+ // @ts-expect-error deliberately invalid for the test
+ expect(() => new Rate(1, "fortnight")).toThrow(/must be one of/);
+ });
+});
+
+describe("Calendar", () => {
+ test("builds the wire body for a Daily cadence", () => {
+ const cal = new Calendar(["09:00", "18:00"], new Daily(), "Europe/Berlin");
+ expect(cal.toRequestBody()).toEqual({
+ calendar: { times_of_day: ["09:00", "18:00"], cadence: { daily: {} } },
+ timezone: "Europe/Berlin",
+ });
+ });
+
+ test("builds the wire body for a Weekly cadence", () => {
+ const cal = new Calendar(["09:00"], new Weekly(["mon", "wed", "fri"]), "Europe/Berlin");
+ expect(cal.toRequestBody().calendar?.cadence).toEqual({
+ weekly: { days: ["mon", "wed", "fri"] },
+ });
+ });
+
+ test("builds the wire body for a Monthly cadence", () => {
+ const cal = new Calendar(["09:00"], new Monthly([1, 15]), "Europe/Berlin");
+ expect(cal.toRequestBody().calendar?.cadence).toEqual({ monthly: { days: [1, 15] } });
+ });
+
+ test("rejects a non-full-hour time", () => {
+ expect(() => new Calendar(["09:30"], new Daily(), "Europe/Berlin")).toThrow(/on the hour/);
+ });
+
+ test("rejects an empty times_of_day", () => {
+ expect(() => new Calendar([], new Daily(), "Europe/Berlin")).toThrow(/non-empty/);
+ });
+
+ test("rejects an invalid timezone", () => {
+ expect(() => new Calendar(["09:00"], new Daily(), "Not/AZone")).toThrow(
+ /not a valid IANA timezone/,
+ );
+ });
+});
+
+describe("Weekly", () => {
+ test("rejects an empty days list", () => {
+ expect(() => new Weekly([])).toThrow(/non-empty/);
+ });
+
+ test("rejects an invalid day name", () => {
+ // @ts-expect-error deliberately invalid for the test
+ expect(() => new Weekly(["monday"])).toThrow(/not a valid day/);
+ });
+});
+
+describe("Monthly", () => {
+ test("rejects an empty days list", () => {
+ expect(() => new Monthly([])).toThrow(/non-empty/);
+ });
+
+ test("rejects a day out of range", () => {
+ expect(() => new Monthly([32])).toThrow(/out of range/);
+ });
+
+ test("rejects a non-integer day", () => {
+ expect(() => new Monthly([1.5])).toThrow(/must be integers/);
+ });
+});
diff --git a/tests/batch-waiters.test.ts b/tests/batch-waiters.test.ts
new file mode 100644
index 0000000..3c5c450
--- /dev/null
+++ b/tests/batch-waiters.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, test } from "vitest";
+import { WaiterFailureError, WaiterTimeoutError, pollUntil } from "../src/batch/waiters";
+
+describe("pollUntil", () => {
+ test("returns immediately when the first fetch is already done", async () => {
+ const result = await pollUntil(() => "done", { isDone: (v) => v === "done" });
+ expect(result).toBe("done");
+ });
+
+ test("retries until isDone is satisfied", async () => {
+ let calls = 0;
+ const result = await pollUntil(
+ () => {
+ calls += 1;
+ return calls;
+ },
+ { isDone: (v) => v >= 3, initialInterval: 0.01, maxInterval: 0.01 },
+ );
+ expect(result).toBe(3);
+ expect(calls).toBe(3);
+ });
+
+ test("throws WaiterFailureError when isFailure matches", async () => {
+ await expect(
+ pollUntil(() => "bad", {
+ isDone: () => false,
+ isFailure: (v) => v === "bad",
+ initialInterval: 0.01,
+ }),
+ ).rejects.toBeInstanceOf(WaiterFailureError);
+ });
+
+ test("throws WaiterTimeoutError once the deadline passes", async () => {
+ await expect(
+ pollUntil(() => "still-waiting", {
+ isDone: () => false,
+ timeout: 0.05,
+ initialInterval: 0.02,
+ maxInterval: 0.02,
+ }),
+ ).rejects.toBeInstanceOf(WaiterTimeoutError);
+ });
+
+ test("backoff never exceeds maxInterval", async () => {
+ const intervals: number[] = [];
+ let last = Date.now();
+ let calls = 0;
+ await pollUntil(
+ () => {
+ const now = Date.now();
+ if (calls > 0) intervals.push(now - last);
+ last = now;
+ calls += 1;
+ return calls;
+ },
+ { isDone: (v) => v >= 4, initialInterval: 0.01, maxInterval: 0.015, backoff: 3, jitter: 0 },
+ );
+ // With backoff=3 and no cap, interval 2 would be ~0.03s; the cap keeps every interval <= ~0.015s (+ scheduling slack).
+ for (const ms of intervals) {
+ expect(ms).toBeLessThan(50);
+ }
+ });
+});
diff --git a/tests/index.test.ts b/tests/index.test.ts
index 1819569..572a333 100644
--- a/tests/index.test.ts
+++ b/tests/index.test.ts
@@ -1,4 +1,4 @@
-import { http } from "msw";
+import { http, HttpResponse } from "msw";
import { type Mock, beforeEach, describe, expect, test, vi } from "vitest"; //TODO(Nestor): Try to use globals instead of importing
import packageJson from "../package.json" with { type: "json" };
import { ZenRows } from "../src";
@@ -71,6 +71,118 @@ describe("ZenRows Client Get", () => {
);
});
+ test("fetch() is the primary method; get() is a deprecated alias for it", async () => {
+ const response = await client.fetch(url);
+ expect(response.status).toBe(200);
+
+ const viaGet = await client.get(url);
+ expect(viaGet.status).toBe(200);
+ });
+
+ test("extract() sets the extract param, defaulting to auto", async () => {
+ const response = await client.extract(url);
+
+ const parsedUrl = new URL(response.url);
+ expect(parsedUrl.searchParams.get("extract")).toBe("auto");
+ });
+
+ test("extract() sends Adaptive Stealth Mode by default", async () => {
+ const response = await client.extract(url);
+
+ const parsedUrl = new URL(response.url);
+ expect(parsedUrl.searchParams.get("mode")).toBe("auto");
+ });
+
+ test("extract() omits mode when adaptiveStealth is disabled", async () => {
+ const response = await client.extract(url, { adaptiveStealth: false });
+
+ const parsedUrl = new URL(response.url);
+ expect(parsedUrl.searchParams.has("mode")).toBe(false);
+ });
+
+ test("extract() accepts an explicit mode", async () => {
+ const response = await client.extract(url, { extract: "native" });
+
+ const parsedUrl = new URL(response.url);
+ expect(parsedUrl.searchParams.get("extract")).toBe("native");
+ });
+
+ describe("extract() AUTH010 fallback", () => {
+ test("retries once with autoparse when the domain isn't Extract-enabled", async () => {
+ let secondCallUrl: URL | undefined;
+ let attempt = 0;
+ server.use(
+ http.get("https://api.zenrows.com/v1/", ({ request }) => {
+ attempt += 1;
+ if (attempt === 1) {
+ return HttpResponse.json(
+ { code: "AUTH010", title: "Domain not enabled" },
+ { status: 402 },
+ );
+ }
+ secondCallUrl = new URL(request.url);
+ return HttpResponse.json([{ found: "via autoparse" }]);
+ }),
+ );
+
+ const response = await client.extract(url);
+
+ expect(response.status).toBe(200);
+ expect(attempt).toBe(2);
+ expect(secondCallUrl?.searchParams.get("autoparse")).toBe("true");
+ expect(secondCallUrl?.searchParams.has("extract")).toBe(false);
+ expect(secondCallUrl?.searchParams.get("mode")).toBe("auto");
+ });
+
+ test("does not retry when fallbackToAutoparse is false", async () => {
+ let attempt = 0;
+ server.use(
+ http.get("https://api.zenrows.com/v1/", () => {
+ attempt += 1;
+ return HttpResponse.json({ code: "AUTH010" }, { status: 402 });
+ }),
+ );
+
+ const response = await client.extract(url, { fallbackToAutoparse: false });
+
+ expect(response.status).toBe(402);
+ expect(attempt).toBe(1);
+ });
+
+ test("does not retry for a 402 that isn't AUTH010 (e.g. out of credits)", async () => {
+ let attempt = 0;
+ server.use(
+ http.get("https://api.zenrows.com/v1/", () => {
+ attempt += 1;
+ return HttpResponse.json(
+ { code: "AUTH004", title: "No credit available" },
+ { status: 402 },
+ );
+ }),
+ );
+
+ const response = await client.extract(url);
+
+ expect(response.status).toBe(402);
+ expect(attempt).toBe(1);
+ });
+
+ test("does not retry for non-auto modes", async () => {
+ let attempt = 0;
+ server.use(
+ http.get("https://api.zenrows.com/v1/", () => {
+ attempt += 1;
+ return HttpResponse.json({ code: "AUTH010" }, { status: 402 });
+ }),
+ );
+
+ const response = await client.extract(url, { extract: "native" });
+
+ expect(response.status).toBe(402);
+ expect(attempt).toBe(1);
+ });
+ });
+
test("should check response status on POST request", async () => {
const response = await client.post(url);
@@ -100,4 +212,22 @@ describe("ZenRows Client Get", () => {
}),
);
});
+
+ test("preserves a non-Content-Type custom header on POST alongside the default Content-Type", async () => {
+ // normalizedHeaders' fallback branch (any header key other than content-type) was never
+ // exercised before — every existing POST test only overrode Content-Type itself.
+ const clientSpy = vi.spyOn(client, "fetchWithRetry");
+
+ await client.post(url, {}, { headers: { "X-Custom-Header": "custom-value" } });
+
+ expect(clientSpy).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ headers: expect.objectContaining({
+ "Content-Type": "application/x-www-form-urlencoded",
+ "X-Custom-Header": "custom-value",
+ }),
+ }),
+ );
+ });
});
diff --git a/vitest.config.ts b/vitest.config.ts
index adce0e6..f1e4a16 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -6,5 +6,9 @@ export default defineConfig({
globals: true,
include: ["tests/**/*.test.ts"],
setupFiles: ["tests/_setup.ts"],
+ coverage: {
+ include: ["src/**"],
+ exclude: ["examples/**"],
+ },
},
});