@datafuel/sdk 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,13 +16,16 @@ const markdown = await df.markdown("https://example.com");
16
16
 
17
17
  ## Pick the call
18
18
 
19
- | You have | Call | Waits? |
20
- | ---------------------------- | ----------------------------------------------------- | ------------- |
21
- | One URL | `scrape` / `markdown` | yes |
22
- | A site, need its URL list | `map` | yes |
23
- | A start URL, need many pages | `crawl`, or `startCrawl` + `waitCrawl` + `crawlPages` | `crawl` does |
24
- | A list of known URLs | `runJob`, or `createJob` + `waitJob` + `jobResults` | `runJob` does |
25
- | A question for an AI engine | `ask` | yes |
19
+ | You have | Call | Waits? |
20
+ | ---------------------------- | ------------------------------------------------------- | ------------- |
21
+ | One URL | `scrape` / `markdown` | yes |
22
+ | A site, need its URL list | `map` | yes |
23
+ | A start URL, need many pages | `crawl`, or `startCrawl` + `waitCrawl` + `crawlPages` | `crawl` does |
24
+ | A list of known URLs | `runJob`, or `createJob` + `waitJob` + `jobResults` | `runJob` does |
25
+ | A question for an AI engine | `ask` | yes |
26
+ | A Google search | `search` | yes |
27
+ | Many prompts or searches | `runAskJob` / `runSearchJob` | yes |
28
+ | Earlier jobs, tasks, usage | `listJobs` / `listTasks` / `analytics` / `transactions` | yes |
26
29
 
27
30
  Start with plain `scrape`. Turn on `jsRendering` only when the page comes back empty: it is slower and costs five times the credits on a Basic proxy. `map` a section before you `crawl` it, it costs one credit and tells you how big it is.
28
31
 
@@ -60,7 +63,9 @@ try {
60
63
 
61
64
  `res.text` returns html or markdown, `res.data` structured output, `res.image` screenshot bytes.
62
65
 
63
- Page options, shared by `scrape`, jobs and crawls: `format` (`html`, `markdown`, `json`, `png`, `jpeg`), `jsRendering`, `waitFor`, `waitForTimeoutMs`, `jsInstructions`, `blockResource`, `mainContentOnly`, `includeImages`, `extract`, `extractRegex`, `template`, `method`, `body`, `contentType`, `headers`, `headerOrder`, `cookies`, `userAgent`, `userAgentType`, `ai`.
66
+ Page options, shared by `scrape`, jobs and crawls: `format` (`html`, `markdown`, `json`, `png`, `jpeg`), `jsRendering`, `waitFor`, `waitForTimeoutMs`, `jsInstructions` (an object keyed by action, e.g. `{ click: "#more" }`; `df.jsInstructions()` lists the actions), `blockResource`, `mainContentOnly`, `includeImages`, `extract`, `extractRegex`, `template`, `method`, `body`, `contentType`, `headers`, `headerOrder`, `cookies`, `userAgent`, `userAgentType`, `ai`.
67
+
68
+ `proxy` takes `type`, `country`, `city`, `state`, `asn`, and a sticky `sessionId` with `ttl` (seconds) on `scrape`, `map`, URL jobs and crawls. `df.proxyLocations()` and `df.proxyAsns(country)` list what a proxy type can exit from.
64
69
 
65
70
  ## AI after the scrape
66
71
 
@@ -108,6 +113,8 @@ Check `stop_reason`: `insufficient_credits` means the crawl ended early. If the
108
113
 
109
114
  Unset limits use the API defaults: 100 pages, depth 3, 5 pages in flight.
110
115
 
116
+ `df.cancelCrawl(id)` stops a crawl: queued pages are refunded, pages in flight finish and bill. `df.cancelJob(id)` does the same for a job. Both throw `JobNotCancellable` once the work has finished.
117
+
111
118
  ## Jobs
112
119
 
113
120
  ```ts
@@ -118,7 +125,23 @@ for (const task of results.tasks) {
118
125
  }
119
126
  ```
120
127
 
121
- `sequential: true` runs the URLs one after the other; the default runs them concurrently, bounded by your account's concurrency limit. `runAskJob(prompts, { engine })` does the same for a batch of prompts.
128
+ `sequential: true` runs the URLs one after the other; the default runs them concurrently, bounded by your account's concurrency limit. `runAskJob(prompts, { engine })` and `runSearchJob(queries)` do the same for a batch of prompts or Google searches. A job needs at least two targets.
129
+
130
+ ## Ask and search
131
+
132
+ ```ts
133
+ const answer = await df.ask("best CRM for a 10-person team", {
134
+ engine: "perplexity", // openai, gemini, google_ai_mode, perplexity, copilot
135
+ websearch: true,
136
+ country: "us",
137
+ });
138
+ console.log(answer.text);
139
+
140
+ const serp = await df.search("best crm", { country: "us", language: "en", page: 1 });
141
+ console.log(serp.data); // parsed results page; format "html" or "markdown" for the raw page
142
+ ```
143
+
144
+ `search` also takes `location` (or `uule`, or `lat`/`lon` with `radius`), `googleDomain`, `tbs`, `safe`, `cr`, `lr`, `nfpr`, `filter` and `proxyCountry`.
122
145
 
123
146
  ## Map
124
147
 
@@ -129,6 +152,26 @@ for (const link of site.links) console.log(link.url);
129
152
 
130
153
  An empty `site.links` comes with a `site.reason`. `no_links_on_page` usually means the navigation is rendered client-side.
131
154
 
155
+ ## Usage and history
156
+
157
+ ```ts
158
+ const usage = await df.analytics({ startDate: "2026-09-01", endDate: "2026-09-30" });
159
+ console.log(usage.summary.success_rate, usage.summary.avg_credits_per_request);
160
+
161
+ const { jobs, nextCursor } = await df.listJobs({ type: "crawl", limit: 20 });
162
+ const failed = await df.listTasks({ jobId: jobs[0]!.id, status: "failed" });
163
+ const history = await df.transactions({ operation: "refund", limit: 50 });
164
+ ```
165
+
166
+ `listJobs` and `listTasks` run newest first; pass `nextCursor` back as `cursor` until it is absent. Task items carry no result: call `getTask(id)` for it. Dates are `YYYY-MM-DD` in UTC (a `Date` is sent as its UTC day) and `endDate` is inclusive. `transactions` pages with `page` and `limit`, and its `sums` total each operation over the whole range. In `analytics`, `status_code` 0 means the target never answered (timeout, DNS).
167
+
168
+ ```ts
169
+ const credits = await df.balance();
170
+ const { plan_balance, payg_balance } = await df.balanceSplit();
171
+ ```
172
+
173
+ `balance` is what you can spend. It is made of plan credits and pay-as-you-go credits. Plan credits are spent first; unused ones roll over when the plan renews and expire if it is not renewed. Pay-as-you-go credits come from one-time credit packs (a `purchase` transaction), are spent after plan credits and never expire. Each transaction's `plan_amount` is the part of `amount` that moved plan credits.
174
+
132
175
  ## Errors
133
176
 
134
177
  ```ts
@@ -148,7 +191,7 @@ try {
148
191
  ```
149
192
 
150
193
  - `NoApiKey`: no key was passed and `DATAFUEL_API_KEY` is empty. Thrown before any request.
151
- - `APIError`: the API refused the request. Subclasses: `Unauthorized`, `InsufficientCredits`, `RateLimited`, `NotFound`, `InvalidAttributes`, `IdempotencyKeyReused`.
194
+ - `APIError`: the API refused the request. `.code` holds the API's error code (typed as `ErrorCode`). Subclasses: `Unauthorized`, `Forbidden`, `InsufficientCredits`, `RateLimited`, `NotFound`, `InvalidAttributes`, `IdempotencyKeyReused`, `JobNotCancellable`.
152
195
  - `ModuleUnavailable`, `EngineUnavailable`: an operator switched a task type or LLM engine off, e.g. during a provider outage. The reason is in the message, nothing is charged, and the SDK does not retry. `df.capabilities()` lists what is on.
153
196
  - `TaskFailed`, and `Blocked` when the target refused: the API accepted the task but the page could not be scraped. The error carries `.result`, so the envelope is still readable. Failed tasks are refunded.
154
197
  - `WaitTimeout`: a wait ran out of time. `.id` picks the work back up.
package/dist/index.cjs CHANGED
@@ -28,9 +28,11 @@ __export(index_exports, {
28
28
  DataFuel: () => DataFuel,
29
29
  DataFuelError: () => DataFuelError,
30
30
  EngineUnavailable: () => EngineUnavailable,
31
+ Forbidden: () => Forbidden,
31
32
  IdempotencyKeyReused: () => IdempotencyKeyReused,
32
33
  InsufficientCredits: () => InsufficientCredits,
33
34
  InvalidAttributes: () => InvalidAttributes,
35
+ JobNotCancellable: () => JobNotCancellable,
34
36
  ModuleUnavailable: () => ModuleUnavailable,
35
37
  NoApiKey: () => NoApiKey,
36
38
  NotFound: () => NotFound,
@@ -71,6 +73,8 @@ var APIError = class extends DataFuelError {
71
73
  };
72
74
  var Unauthorized = class extends APIError {
73
75
  };
76
+ var Forbidden = class extends APIError {
77
+ };
74
78
  var NotFound = class extends APIError {
75
79
  };
76
80
  var RateLimited = class extends APIError {
@@ -81,6 +85,8 @@ var InvalidAttributes = class extends APIError {
81
85
  };
82
86
  var IdempotencyKeyReused = class extends APIError {
83
87
  };
88
+ var JobNotCancellable = class extends APIError {
89
+ };
84
90
  var Unavailable = class extends APIError {
85
91
  };
86
92
  var ModuleUnavailable = class extends Unavailable {
@@ -116,12 +122,16 @@ var BY_CODE = {
116
122
  INSUFFICIENT_CREDITS: InsufficientCredits,
117
123
  INVALID_ATTRIBUTES: InvalidAttributes,
118
124
  IDEMPOTENCY_KEY_REUSED: IdempotencyKeyReused,
119
- INVALID_API_KEY: Unauthorized
125
+ INVALID_API_KEY: Unauthorized,
126
+ FORBIDDEN: Forbidden,
127
+ JOB_NOT_CANCELLABLE: JobNotCancellable
120
128
  };
121
129
  var BY_STATUS = {
122
130
  401: Unauthorized,
123
131
  402: InsufficientCredits,
132
+ 403: Forbidden,
124
133
  404: NotFound,
134
+ 409: JobNotCancellable,
125
135
  422: IdempotencyKeyReused,
126
136
  429: RateLimited,
127
137
  503: Unavailable
@@ -142,24 +152,26 @@ function apiError(status, body, retryAfter = 0) {
142
152
 
143
153
  // src/core.ts
144
154
  var DEFAULT_BASE_URL = "https://scraping-api.datafuel.ai/api/v1";
145
- var VERSION = "0.1.0";
155
+ var VERSION = "0.3.0";
146
156
  var DEFAULT_TIMEOUT_MS = 18e4;
147
157
  var STILL_PROCESSING_DELAY_MS = 2e3;
148
158
  var STILL_PROCESSING = "TASK_STILL_PROCESSING";
149
159
  var MAX_IDEMPOTENCY_KEY = 255;
150
160
  var Request = class {
151
- constructor(method, path, params, body, idempotencyKey) {
161
+ constructor(method, path, params, body, idempotencyKey, auth = true) {
152
162
  this.method = method;
153
163
  this.path = path;
154
164
  this.params = params;
155
165
  this.body = body;
156
166
  this.idempotencyKey = idempotencyKey;
167
+ this.auth = auth;
157
168
  }
158
169
  method;
159
170
  path;
160
171
  params;
161
172
  body;
162
173
  idempotencyKey;
174
+ auth;
163
175
  /** GETs are safe by nature, writes because they carry an idempotency key. */
164
176
  get retryable() {
165
177
  return this.method === "GET" || this.idempotencyKey !== void 0;
@@ -186,10 +198,10 @@ function newIdempotencyKey() {
186
198
  }
187
199
  function headers(apiKey, userAgent, request) {
188
200
  const out = {
189
- "X-API-Key": apiKey,
190
201
  Accept: "application/json",
191
202
  "User-Agent": userAgent
192
203
  };
204
+ if (apiKey) out["X-API-Key"] = apiKey;
193
205
  if (request.body !== void 0) out["Content-Type"] = "application/json";
194
206
  if (request.idempotencyKey !== void 0) out["Idempotency-Key"] = request.idempotencyKey;
195
207
  return out;
@@ -285,7 +297,7 @@ function buildScrape(url, options, proxy, idempotencyKey) {
285
297
  );
286
298
  }
287
299
  function buildJob(urls, options, proxy, sequential, idempotencyKey) {
288
- const attrs = { ...scrapeAttributes(options), urls: [...urls] };
300
+ const attrs = { ...scrapeAttributes(options), urls: [...urls], ...proxySession(proxy) };
289
301
  return new Request(
290
302
  "POST",
291
303
  "/job",
@@ -299,6 +311,7 @@ function askAttributes(promptField, prompt, options) {
299
311
  if (options.websearch) attrs.websearch = true;
300
312
  if (options.followUp) attrs.follow_up_prompt = options.followUp;
301
313
  if (options.country) attrs.proxy_country = options.country;
314
+ if (options.location) attrs.location = options.location;
302
315
  if (options.format) attrs.result_format = options.format;
303
316
  return attrs;
304
317
  }
@@ -341,6 +354,7 @@ function buildCrawl(url, options, proxy, idempotencyKey) {
341
354
  if (options.excludePaths?.length) attrs.exclude_paths = [...options.excludePaths];
342
355
  if (options.includeSubdomains) attrs.include_subdomains = true;
343
356
  if (options.allowBackwardLinks) attrs.allow_backward_links = true;
357
+ Object.assign(attrs, proxySession(proxy));
344
358
  return new Request(
345
359
  "POST",
346
360
  "/crawl",
@@ -349,6 +363,52 @@ function buildCrawl(url, options, proxy, idempotencyKey) {
349
363
  idempotencyKey
350
364
  );
351
365
  }
366
+ function searchAttributes(queryField, query2, options) {
367
+ const attrs = { [queryField]: query2 };
368
+ const strings = [
369
+ ["country", "country"],
370
+ ["language", "language"],
371
+ ["location", "location"],
372
+ ["googleDomain", "google_domain"],
373
+ ["uule", "uule"],
374
+ ["cr", "cr"],
375
+ ["lr", "lr"],
376
+ ["tbs", "tbs"],
377
+ ["safe", "safe"],
378
+ ["uds", "uds"],
379
+ ["kgmid", "kgmid"],
380
+ ["si", "si"],
381
+ ["ludocid", "ludocid"],
382
+ ["lsig", "lsig"],
383
+ ["ibp", "ibp"],
384
+ ["proxyCountry", "proxy_country"],
385
+ ["format", "result_format"]
386
+ ];
387
+ for (const [option, attribute] of strings) {
388
+ if (options[option]) attrs[attribute] = options[option];
389
+ }
390
+ if (options.page) attrs.page = options.page;
391
+ if (options.radius) attrs.radius = options.radius;
392
+ if (options.lat !== void 0) attrs.lat = options.lat;
393
+ if (options.lon !== void 0) attrs.lon = options.lon;
394
+ if (options.nfpr !== void 0) attrs.nfpr = options.nfpr;
395
+ if (options.filter !== void 0) attrs.filter = options.filter;
396
+ return attrs;
397
+ }
398
+ function buildSearch(query2, options, idempotencyKey) {
399
+ const attrs = searchAttributes("query", query2, options);
400
+ return new Request("POST", "/task", void 0, envelope("serp", attrs), idempotencyKey);
401
+ }
402
+ function buildSearchJob(queries, options, sequential, idempotencyKey) {
403
+ const attrs = searchAttributes("queries", [...queries], options);
404
+ return new Request(
405
+ "POST",
406
+ "/job",
407
+ void 0,
408
+ envelope("serp", attrs, { multithreaded: !sequential }),
409
+ idempotencyKey
410
+ );
411
+ }
352
412
  function crawlResultsRequest(crawlId, cursor, limit) {
353
413
  const params = {};
354
414
  if (cursor) params.cursor = cursor;
@@ -359,6 +419,54 @@ function crawlResultsRequest(crawlId, cursor, limit) {
359
419
  Object.keys(params).length > 0 ? params : void 0
360
420
  );
361
421
  }
422
+ function day(value) {
423
+ return typeof value === "string" ? value : value.toISOString().slice(0, 10);
424
+ }
425
+ function query(entries) {
426
+ const params = {};
427
+ for (const [name, value] of entries) {
428
+ if (value === void 0 || value === "") continue;
429
+ params[name] = value instanceof Date ? day(value) : String(value);
430
+ }
431
+ return Object.keys(params).length > 0 ? params : void 0;
432
+ }
433
+ function listRequest(path, options, jobId) {
434
+ const params = query([
435
+ ["status", options.status],
436
+ ["type", options.type],
437
+ ["job_id", jobId],
438
+ ["start_date", options.startDate],
439
+ ["end_date", options.endDate],
440
+ ["limit", options.limit],
441
+ ["cursor", options.cursor]
442
+ ]);
443
+ return new Request("GET", path, params);
444
+ }
445
+ function listJobsRequest(options) {
446
+ return listRequest("/job", options);
447
+ }
448
+ function listTasksRequest(options) {
449
+ return listRequest("/task", options, options.jobId);
450
+ }
451
+ function transactionsRequest(options) {
452
+ const params = query([
453
+ ["operation", options.operation],
454
+ ["start_date", options.startDate],
455
+ ["end_date", options.endDate],
456
+ ["page", options.page],
457
+ ["limit", options.limit]
458
+ ]);
459
+ return new Request("GET", "/users/@me/transactions", params);
460
+ }
461
+ function analyticsRequest(options) {
462
+ const params = query([
463
+ ["start_date", options.startDate],
464
+ ["end_date", options.endDate],
465
+ ["interval", options.interval],
466
+ ["module", options.module]
467
+ ]);
468
+ return new Request("GET", "/task/analytics/dashboard", params);
469
+ }
362
470
  function stillProcessing(body) {
363
471
  return body !== null && typeof body === "object" && body.code === STILL_PROCESSING;
364
472
  }
@@ -522,7 +630,7 @@ var DataFuel = class {
522
630
  return new Promise((resolve) => setTimeout(resolve, ms));
523
631
  }
524
632
  async send(request, opts = {}) {
525
- if (!this.apiKey) {
633
+ if (!this.apiKey && request.auth) {
526
634
  throw new NoApiKey("no API key: pass one to the client or set DATAFUEL_API_KEY");
527
635
  }
528
636
  const url = new URL(this.baseUrl + request.path);
@@ -626,6 +734,11 @@ var DataFuel = class {
626
734
  const request = buildAsk(prompt, options, key(options.idempotencyKey));
627
735
  return this.runTask(request, options);
628
736
  }
737
+ /** Run a Google search and return the results page, parsed to JSON by default. */
738
+ async search(query2, options = {}) {
739
+ const request = buildSearch(query2, options, key(options.idempotencyKey));
740
+ return this.runTask(request, options);
741
+ }
629
742
  /**
630
743
  * List the URLs of a site without scraping them.
631
744
  *
@@ -650,6 +763,18 @@ var DataFuel = class {
650
763
  const request = buildCrawl(url, options, options.proxy, key(options.idempotencyKey));
651
764
  return strField(await this.send(request, options), "job_id");
652
765
  }
766
+ /**
767
+ * Stop a crawl. Queued pages are refunded, pages in flight finish and bill.
768
+ * Throws {@link JobNotCancellable} when it already finished.
769
+ */
770
+ async cancelCrawl(crawlId, options = {}) {
771
+ return cancelResult(
772
+ await this.send(
773
+ new Request("POST", `/crawl/${pathSegment(crawlId)}/cancel`),
774
+ options
775
+ )
776
+ );
777
+ }
653
778
  /** Return the progress of a crawl. */
654
779
  async getCrawl(crawlId, options = {}) {
655
780
  const body = record(
@@ -760,20 +885,30 @@ var DataFuel = class {
760
885
  );
761
886
  return strField(await this.send(request, options), "id");
762
887
  }
888
+ /** Queue a batch of Google searches and return the job id. */
889
+ async createSearchJob(queries, options = {}) {
890
+ const request = buildSearchJob(
891
+ queries,
892
+ options,
893
+ options.sequential ?? false,
894
+ key(options.idempotencyKey)
895
+ );
896
+ return strField(await this.send(request, options), "id");
897
+ }
763
898
  /** Return the progress of a job. */
764
899
  async getJob(jobId, options = {}) {
765
- const body = record(
900
+ return jobStatus(
766
901
  await this.send(new Request("GET", `/job/${pathSegment(jobId)}`), options)
767
902
  );
768
- const raw = body;
769
- return {
770
- status: raw.status,
771
- tasks_count: raw.tasks_count ?? 0,
772
- tasks_done: raw.tasks_done ?? 0,
773
- tasks_remaining: raw.tasks_remaining ?? 0,
774
- total_cost: raw.total_cost ?? 0,
775
- done: isDone(raw.status)
776
- };
903
+ }
904
+ /**
905
+ * Stop a job. Queued tasks are refunded, tasks in flight finish and bill.
906
+ * Throws {@link JobNotCancellable} when it already finished.
907
+ */
908
+ async cancelJob(jobId, options = {}) {
909
+ return cancelResult(
910
+ await this.send(new Request("POST", `/job/${pathSegment(jobId)}/cancel`), options)
911
+ );
777
912
  }
778
913
  /**
779
914
  * Return the per-task results of a job.
@@ -794,6 +929,33 @@ var DataFuel = class {
794
929
  tasks: tasks.map((task) => new Result(record(task)))
795
930
  };
796
931
  }
932
+ /** One page of your jobs and crawls, newest first. Pass `nextCursor` back as `cursor`. */
933
+ async listJobs(options = {}) {
934
+ const body = record(await this.send(listJobsRequest(options), options));
935
+ const next = typeof body.next_cursor === "string" ? body.next_cursor : void 0;
936
+ return {
937
+ jobs: array(body.jobs).map((raw) => {
938
+ const job = record(raw);
939
+ return { ...job, ...jobStatus(job) };
940
+ }),
941
+ ...next ? { nextCursor: next } : {}
942
+ };
943
+ }
944
+ /**
945
+ * One page of your tasks, newest first, including those of jobs and crawls.
946
+ * Items carry no result: fetch it with {@link getTask}.
947
+ */
948
+ async listTasks(options = {}) {
949
+ const body = record(await this.send(listTasksRequest(options), options));
950
+ const next = typeof body.next_cursor === "string" ? body.next_cursor : void 0;
951
+ return {
952
+ tasks: array(body.tasks).map((raw) => {
953
+ const task = record(raw);
954
+ return { ...task, job_id: task.job_id ?? null };
955
+ }),
956
+ ...next ? { nextCursor: next } : {}
957
+ };
958
+ }
797
959
  /** Poll until the job is done. Without `timeoutMs` it waits indefinitely. */
798
960
  async waitJob(jobId, options = {}) {
799
961
  const { timeoutMs, ...perPoll } = options;
@@ -819,18 +981,90 @@ var DataFuel = class {
819
981
  await this.waitJob(id, options);
820
982
  return this.jobResults(id, options);
821
983
  }
984
+ /** Create a search job, wait for it, and return its results. */
985
+ async runSearchJob(queries, options = {}) {
986
+ const id = await this.createSearchJob(queries, options);
987
+ await this.waitJob(id, options);
988
+ return this.jobResults(id, options);
989
+ }
822
990
  // --- account -----------------------------------------------------------
823
- /** Which task types and LLM engines are switched on right now. */
991
+ /** Which task types and LLM engines are switched on right now. Needs no key. */
824
992
  async capabilities(options = {}) {
825
- return new Capabilities(
826
- record(await this.send(new Request("GET", "/capabilities"), options))
993
+ const request = new Request(
994
+ "GET",
995
+ "/config/capabilities",
996
+ void 0,
997
+ void 0,
998
+ void 0,
999
+ false
1000
+ );
1001
+ return new Capabilities(record(await this.send(request, options)));
1002
+ }
1003
+ /** The browser actions `jsInstructions` accepts, with their arguments. Needs no key. */
1004
+ async jsInstructions(options = {}) {
1005
+ const request = new Request(
1006
+ "GET",
1007
+ "/config/js-instructions",
1008
+ void 0,
1009
+ void 0,
1010
+ void 0,
1011
+ false
1012
+ );
1013
+ const body = record(await this.send(request, options));
1014
+ return Array.isArray(body.instructions) ? body.instructions : [];
1015
+ }
1016
+ /** Countries, regions and cities a proxy type can exit from. */
1017
+ async proxyLocations(options = {}) {
1018
+ const params = options.proxyType ? { proxy_type: options.proxyType } : void 0;
1019
+ return list(
1020
+ await this.send(new Request("GET", "/config/proxy/locations", params), options)
1021
+ );
1022
+ }
1023
+ /** ASNs a proxy type can exit from in one country (ISO 3166-1 alpha-2). */
1024
+ async proxyAsns(country, options = {}) {
1025
+ const params = { country };
1026
+ if (options.proxyType) params.proxy_type = options.proxyType;
1027
+ return list(
1028
+ await this.send(new Request("GET", "/config/proxy/asn", params), options)
827
1029
  );
828
1030
  }
829
- /** Remaining credits. */
1031
+ /** Remaining credits: plan and pay-as-you-go together. */
830
1032
  async balance(options = {}) {
831
1033
  const body = await this.send(new Request("GET", "/users/@me/balance"), options);
832
1034
  return intField(body, "balance");
833
1035
  }
1036
+ /** Remaining credits by pool: plan credits (spent first) and pay-as-you-go credits. */
1037
+ async balanceSplit(options = {}) {
1038
+ const body = await this.send(new Request("GET", "/users/@me/balance"), options);
1039
+ return {
1040
+ balance: intField(body, "balance"),
1041
+ plan_balance: intField(body, "plan_balance"),
1042
+ payg_balance: intField(body, "payg_balance")
1043
+ };
1044
+ }
1045
+ /**
1046
+ * Credit movements, newest first: plan assignments, credit pack purchases, usage, refunds, expiry.
1047
+ * `sums` totals each operation over the whole range, not just this page.
1048
+ */
1049
+ async transactions(options = {}) {
1050
+ const body = record(await this.send(transactionsRequest(options), options));
1051
+ return {
1052
+ transactions: array(body.transactions),
1053
+ total_count: Number(body.total_count ?? 0),
1054
+ sums: array(body.sums)
1055
+ };
1056
+ }
1057
+ /** Usage over a date range: totals, a time series and breakdowns. Default: the last 30 days. */
1058
+ async analytics(options = {}) {
1059
+ const body = record(await this.send(analyticsRequest(options), options));
1060
+ return {
1061
+ ...body,
1062
+ timeseries: array(body.timeseries),
1063
+ by_module: array(body.by_module),
1064
+ top_targets: array(body.top_targets),
1065
+ by_status_code: array(body.by_status_code)
1066
+ };
1067
+ }
834
1068
  /** The account behind the API key. */
835
1069
  async me(options = {}) {
836
1070
  const body = await this.send(new Request("GET", "/users/@me"), options);
@@ -876,6 +1110,34 @@ function record(body) {
876
1110
  }
877
1111
  return body;
878
1112
  }
1113
+ function list(body) {
1114
+ if (!Array.isArray(body)) {
1115
+ throw new DataFuelError(`unexpected answer from the API: ${JSON.stringify(body) ?? "empty"}`);
1116
+ }
1117
+ return body;
1118
+ }
1119
+ function array(value) {
1120
+ return Array.isArray(value) ? value : [];
1121
+ }
1122
+ function jobStatus(body) {
1123
+ const raw = record(body);
1124
+ return {
1125
+ status: raw.status,
1126
+ tasks_count: raw.tasks_count ?? 0,
1127
+ tasks_done: raw.tasks_done ?? 0,
1128
+ tasks_remaining: raw.tasks_remaining ?? 0,
1129
+ total_cost: raw.total_cost ?? 0,
1130
+ done: isDone(raw.status)
1131
+ };
1132
+ }
1133
+ function cancelResult(body) {
1134
+ const raw = record(body);
1135
+ return {
1136
+ ...jobStatus(body),
1137
+ refunded_tasks: raw.refunded_tasks ?? 0,
1138
+ refunded_credits: raw.refunded_credits ?? 0
1139
+ };
1140
+ }
879
1141
  function field(body, name) {
880
1142
  const value = record(body)[name];
881
1143
  if (value === void 0 || value === null) {
@@ -903,9 +1165,11 @@ function intField(body, name) {
903
1165
  DataFuel,
904
1166
  DataFuelError,
905
1167
  EngineUnavailable,
1168
+ Forbidden,
906
1169
  IdempotencyKeyReused,
907
1170
  InsufficientCredits,
908
1171
  InvalidAttributes,
1172
+ JobNotCancellable,
909
1173
  ModuleUnavailable,
910
1174
  NoApiKey,
911
1175
  NotFound,