@getanyapi/sdk 0.9.1 → 0.9.5

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
@@ -126,13 +126,30 @@ Per-call transport overrides: `timeoutMs`, `maxRetries`, and an `AbortSignal` vi
126
126
  | `ResultNotFoundError` | - | `unwrap` on an empty found-data result |
127
127
  | `RateLimitedError` | 429 | Too many requests (retried automatically) |
128
128
  | `UpstreamError` | 502 | An upstream backend failed |
129
- | `ConnectionError` | 0 | Network or transport failure (retried) |
129
+ | `ConnectionError` | 0 | Network or transport failure |
130
130
  | `TimeoutError` | 0 | Request exceeded its timeout (not retried) |
131
131
 
132
132
  All extend `AnyAPIError` (with `status` and `requestId`). Retries cover only 429 and network
133
- failures, with jittered exponential backoff honoring `Retry-After`. Default `maxRetries` is 2
134
- (up to 3 attempts); set it on the client or per request. Timeouts are never retried. Configure
135
- with `new AnyAPI({ timeoutMs, maxRetries })`.
133
+ failures proven to happen before a request was sent, with jittered exponential backoff honoring
134
+ `Retry-After`. Default `maxRetries` is 2 (up to 3 attempts); set it on the client or per request.
135
+ Timeouts are never retried. Connection failures during or after a billed `POST /v1/run` are not
136
+ retried because the call may already have been charged. When the send phase is unknown, the SDK
137
+ does not retry. Configure with `new AnyAPI({ timeoutMs, maxRetries })`.
138
+
139
+ Automatic network retry of a billed `run()` requires structured runtime evidence that the
140
+ request body was not sent:
141
+
142
+ | Runtime | Automatic billed-run network retry | Evidence available to the SDK |
143
+ | ------- | ---------------------------------- | ----------------------------- |
144
+ | Node 18+ with built-in undici `fetch` | Yes | DNS and connect codes, connect-phase timeouts, or an undici socket reporting zero bytes written |
145
+ | Bun 1.3.11 | Yes | `ConnectionRefused`, which Bun 1.3.11 emits only while establishing the origin or proxy connection |
146
+ | Cloudflare Workers | No | `retryable: true` means transient, not undelivered; it can appear after the origin received the full body |
147
+ | Deno | No | Fetch exposes only prose without a structured connection code |
148
+ | Browsers | No | Fetch generally exposes an opaque `TypeError` |
149
+
150
+ On a runtime without strict non-delivery evidence, the SDK makes no automatic network retry for a
151
+ billed `run()`. HTTP 429 retry is unchanged. Handle other retries explicitly only when your
152
+ application can establish non-delivery.
136
153
 
137
154
  ## Agent signup
138
155
 
package/dist/index.cjs CHANGED
@@ -103,13 +103,18 @@ var AnyAPIError = class extends Error {
103
103
  status;
104
104
  /** The x-request-id response header when present, else undefined. */
105
105
  requestId;
106
- constructor(message, status, requestId) {
106
+ /** Stable gateway error code when the JSON body includes one, else undefined. */
107
+ code;
108
+ constructor(message, status, requestId, code) {
107
109
  super(message);
108
110
  this.name = new.target.name;
109
111
  this.status = status;
110
112
  if (requestId !== void 0) {
111
113
  this.requestId = requestId;
112
114
  }
115
+ if (code !== void 0) {
116
+ this.code = code;
117
+ }
113
118
  Object.setPrototypeOf(this, new.target.prototype);
114
119
  }
115
120
  };
@@ -131,25 +136,72 @@ var ConnectionError = class extends AnyAPIError {
131
136
  };
132
137
  var TimeoutError = class extends AnyAPIError {
133
138
  };
134
- function errorFromStatus(status, message, requestId) {
139
+ function errorFromStatus(status, message, requestId, code) {
135
140
  switch (status) {
136
141
  case 400:
137
- return new BadRequestError(message, status, requestId);
142
+ return new BadRequestError(message, status, requestId, code);
138
143
  case 401:
139
- return new AuthenticationError(message, status, requestId);
144
+ return new AuthenticationError(message, status, requestId, code);
140
145
  case 402:
141
- return new InsufficientBalanceError(message, status, requestId);
146
+ return new InsufficientBalanceError(message, status, requestId, code);
142
147
  case 404:
143
- return new NotFoundError(message, status, requestId);
148
+ return new NotFoundError(message, status, requestId, code);
144
149
  case 429:
145
- return new RateLimitedError(message, status, requestId);
150
+ return new RateLimitedError(message, status, requestId, code);
146
151
  case 502:
147
- return new UpstreamError(message, status, requestId);
152
+ return new UpstreamError(message, status, requestId, code);
148
153
  default:
149
- return new AnyAPIError(message, status, requestId);
154
+ return new AnyAPIError(message, status, requestId, code);
150
155
  }
151
156
  }
152
157
 
158
+ // src/core/idempotency.ts
159
+ var MAX_IDEMPOTENCY_KEY_BYTES = 255;
160
+ function generateIdempotencyKey() {
161
+ let runtimeCrypto;
162
+ try {
163
+ runtimeCrypto = globalThis.crypto;
164
+ } catch {
165
+ runtimeCrypto = void 0;
166
+ }
167
+ if (typeof runtimeCrypto?.randomUUID === "function") {
168
+ try {
169
+ return runtimeCrypto.randomUUID();
170
+ } catch {
171
+ }
172
+ }
173
+ if (typeof runtimeCrypto?.getRandomValues === "function") {
174
+ try {
175
+ const bytes = runtimeCrypto.getRandomValues(new Uint8Array(16));
176
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
177
+ } catch {
178
+ }
179
+ }
180
+ return Array.from(
181
+ { length: 4 },
182
+ () => Math.floor(Math.random() * 4294967296).toString(16).padStart(8, "0")
183
+ ).join("");
184
+ }
185
+ function validateIdempotencyKey(key) {
186
+ if (key.length === 0 || key.length > MAX_IDEMPOTENCY_KEY_BYTES || [...key].some((char) => {
187
+ const code = char.charCodeAt(0);
188
+ return code < 33 || code > 126;
189
+ })) {
190
+ throw new TypeError(
191
+ "idempotencyKey must be 1-255 bytes of visible ASCII (0x21-0x7e)"
192
+ );
193
+ }
194
+ }
195
+ function pageIdempotencyKey(key, pageNumber) {
196
+ validateIdempotencyKey(key);
197
+ const suffix = `-p${pageNumber}`;
198
+ const prefixLength = MAX_IDEMPOTENCY_KEY_BYTES - suffix.length;
199
+ if (prefixLength < 1) {
200
+ throw new TypeError("pagination page number is too large for an idempotency key");
201
+ }
202
+ return `${key.slice(0, prefixLength)}${suffix}`;
203
+ }
204
+
153
205
  // src/core/account.ts
154
206
  var DEFAULT_BASE_URL = "https://api.getanyapi.com";
155
207
  function malformed(path) {
@@ -458,14 +510,18 @@ async function agentSignup(options = {}) {
458
510
  const text = await response.text().catch(() => "");
459
511
  if (response.status !== 200) {
460
512
  let message = `request failed with status ${response.status}`;
513
+ let code;
461
514
  try {
462
515
  const parsed2 = JSON.parse(text);
463
516
  if (typeof parsed2.error === "string" && parsed2.error !== "") {
464
517
  message = parsed2.error;
465
518
  }
519
+ if (typeof parsed2.code === "string" && parsed2.code !== "") {
520
+ code = parsed2.code;
521
+ }
466
522
  } catch {
467
523
  }
468
- throw errorFromStatus(response.status, message, requestId);
524
+ throw errorFromStatus(response.status, message, requestId, code);
469
525
  }
470
526
  const parsed = JSON.parse(text);
471
527
  return {
@@ -482,6 +538,18 @@ var DEFAULT_TIMEOUT_MS = 6e4;
482
538
  var DEFAULT_MAX_RETRIES = 2;
483
539
  var RETRY_BASE_DELAY_MS = 500;
484
540
  var RETRY_MAX_DELAY_MS = 8e3;
541
+ var PRE_SEND_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
542
+ "EADDRNOTAVAIL",
543
+ "EAI_AGAIN",
544
+ "EAI_NODATA",
545
+ "EAI_NONAME",
546
+ "ECONNREFUSED",
547
+ "EHOSTUNREACH",
548
+ "ENETUNREACH",
549
+ "ENOTFOUND",
550
+ "UND_ERR_CONNECT_TIMEOUT",
551
+ "ConnectionRefused"
552
+ ]);
485
553
  function envApiKey() {
486
554
  try {
487
555
  if (typeof process !== "undefined" && process?.env) {
@@ -566,6 +634,33 @@ function isTimeoutSignal(timeoutSignal, callerSignal) {
566
634
  }
567
635
  return callerSignal?.aborted !== true || timeoutSignal.aborted;
568
636
  }
637
+ function isDefinitelyPreSendConnectionError(error) {
638
+ const seen = /* @__PURE__ */ new Set();
639
+ const visit = (value) => {
640
+ if (typeof value !== "object" && typeof value !== "function" || value === null) {
641
+ return false;
642
+ }
643
+ if (seen.has(value)) {
644
+ return false;
645
+ }
646
+ seen.add(value);
647
+ const candidate = value;
648
+ if (typeof candidate.code === "string" && PRE_SEND_NETWORK_ERROR_CODES.has(candidate.code)) {
649
+ return true;
650
+ }
651
+ if (candidate.code === "ETIMEDOUT" && candidate.syscall === "connect") {
652
+ return true;
653
+ }
654
+ if (candidate.code === "UND_ERR_SOCKET" && candidate.socket?.bytesWritten === 0) {
655
+ return true;
656
+ }
657
+ if (Array.isArray(candidate.errors) && candidate.errors.length > 0) {
658
+ return candidate.errors.some((item) => visit(item)) || visit(candidate.cause);
659
+ }
660
+ return visit(candidate.cause);
661
+ };
662
+ return visit(error);
663
+ }
569
664
  function buildUrl(baseUrl, slug, options) {
570
665
  const base = baseUrl.replace(/\/+$/, "");
571
666
  const url = new URL(`${base}/v1/run/${slug}`);
@@ -584,13 +679,21 @@ function messageFromBody(body, status) {
584
679
  if (body) {
585
680
  try {
586
681
  const parsed = JSON.parse(body);
682
+ const code = typeof parsed.code === "string" && parsed.code !== "" ? parsed.code : void 0;
587
683
  if (typeof parsed.error === "string" && parsed.error !== "") {
588
- return parsed.error;
684
+ return {
685
+ message: parsed.error,
686
+ ...code !== void 0 ? { code } : {}
687
+ };
589
688
  }
689
+ return {
690
+ message: `request failed with status ${status}`,
691
+ ...code !== void 0 ? { code } : {}
692
+ };
590
693
  } catch {
591
694
  }
592
695
  }
593
- return `request failed with status ${status}`;
696
+ return { message: `request failed with status ${status}` };
594
697
  }
595
698
  var AnyAPI = class {
596
699
  apiKey;
@@ -598,6 +701,7 @@ var AnyAPI = class {
598
701
  fetchImpl;
599
702
  maxRetries;
600
703
  timeoutMs;
704
+ idempotency;
601
705
  /**
602
706
  * The network seam the generated per-platform namespaces target. The base client IS a
603
707
  * ClientCore (it implements `run`), so the generated subclass hands `this._core` to each
@@ -620,6 +724,11 @@ var AnyAPI = class {
620
724
  this.fetchImpl = resolvedFetch;
621
725
  this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
622
726
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
727
+ const idempotency = options.idempotency ?? "auto";
728
+ if (idempotency !== "auto" && idempotency !== "off") {
729
+ throw new TypeError('idempotency must be "auto" or "off"');
730
+ }
731
+ this.idempotency = idempotency;
623
732
  }
624
733
  /**
625
734
  * Generic run for any SKU by slug (the untyped network seam + the string fallback). The
@@ -627,13 +736,15 @@ var AnyAPI = class {
627
736
  * base signature is the fallback that returns RunResult<unknown> for an unknown slug.
628
737
  */
629
738
  run(slug, input, options) {
739
+ const body = JSON.stringify(input ?? {});
630
740
  return this.request(
631
741
  "POST",
632
742
  buildUrl(this.baseUrl, slug, options),
633
743
  {
634
- body: JSON.stringify(input ?? {}),
744
+ body,
635
745
  timeoutMs: options?.timeoutMs ?? this.timeoutMs,
636
746
  maxRetries: options?.maxRetries ?? this.maxRetries,
747
+ ...options?.idempotencyKey !== void 0 ? { idempotencyKey: options.idempotencyKey } : {},
637
748
  ...options?.signal ? { signal: options.signal } : {}
638
749
  }
639
750
  );
@@ -698,6 +809,12 @@ var AnyAPI = class {
698
809
  if (this.apiKey) {
699
810
  headers["Authorization"] = `Bearer ${this.apiKey}`;
700
811
  }
812
+ const billedPost = method === "POST" && opts.body !== void 0;
813
+ if (billedPost && this.idempotency === "auto") {
814
+ const key = opts.idempotencyKey ?? generateIdempotencyKey();
815
+ validateIdempotencyKey(key);
816
+ headers["Idempotency-Key"] = key;
817
+ }
701
818
  let attempt = 0;
702
819
  for (; ; ) {
703
820
  const { signal, timeoutSignal } = composeSignal(
@@ -723,7 +840,9 @@ var AnyAPI = class {
723
840
  err instanceof Error ? err.message : "connection failed",
724
841
  0
725
842
  );
726
- if (attempt < opts.maxRetries) {
843
+ const requestMayBeBilled = method === "POST" && opts.body !== void 0;
844
+ const safeToRetry = !requestMayBeBilled || isDefinitelyPreSendConnectionError(err);
845
+ if (safeToRetry && attempt < opts.maxRetries) {
727
846
  await sleep(backoffDelay(attempt), opts.signal);
728
847
  attempt += 1;
729
848
  continue;
@@ -744,7 +863,7 @@ var AnyAPI = class {
744
863
  }
745
864
  }
746
865
  const body = await response.text().catch(() => "");
747
- const message = messageFromBody(body, response.status);
866
+ const { message, code } = messageFromBody(body, response.status);
748
867
  if (response.status === 429 && attempt < opts.maxRetries) {
749
868
  const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
750
869
  const delay = retryAfter ?? backoffDelay(attempt);
@@ -752,7 +871,7 @@ var AnyAPI = class {
752
871
  attempt += 1;
753
872
  continue;
754
873
  }
755
- throw errorFromStatus(response.status, message, requestId);
874
+ throw errorFromStatus(response.status, message, requestId, code);
756
875
  }
757
876
  }
758
877
  /** Internal accessor for GET helpers in account.ts (same base URL / machinery). */
@@ -797,12 +916,14 @@ function paginate(core, slug, input, itemsField, bare, options) {
797
916
  const maxItems = options?.maxItems;
798
917
  async function* walkPages() {
799
918
  let cursor;
919
+ let pageNumber = 1;
800
920
  for (; ; ) {
801
921
  const pageInput = { ...input };
802
922
  if (cursor !== void 0) {
803
923
  pageInput["cursor"] = cursor;
804
924
  }
805
- const result = await core.run(slug, pageInput, wireOptions);
925
+ const pageOptions = optionsForPage(wireOptions, pageNumber);
926
+ const result = await core.run(slug, pageInput, pageOptions);
806
927
  yield result;
807
928
  const data = pageData(result, bare);
808
929
  if (data === null) {
@@ -813,6 +934,7 @@ function paginate(core, slug, input, itemsField, bare, options) {
813
934
  return;
814
935
  }
815
936
  cursor = next;
937
+ pageNumber += 1;
816
938
  }
817
939
  }
818
940
  async function* walkItems() {
@@ -844,6 +966,15 @@ function paginate(core, slug, input, itemsField, bare, options) {
844
966
  };
845
967
  return paginator;
846
968
  }
969
+ function optionsForPage(options, pageNumber) {
970
+ if (options?.idempotencyKey === void 0) {
971
+ return options;
972
+ }
973
+ return {
974
+ ...options,
975
+ idempotencyKey: pageIdempotencyKey(options.idempotencyKey, pageNumber)
976
+ };
977
+ }
847
978
  function stripMaxItems(options) {
848
979
  if (!options) {
849
980
  return void 0;
@@ -1421,7 +1552,7 @@ var EmailNamespace = class {
1421
1552
  /**
1422
1553
  * Email Verifier
1423
1554
  *
1424
- * Verify any email address for deliverability: syntax, domain, and mailbox checks in one normalized response.
1555
+ * Verify an email address for deliverability: a status verdict (valid, risky, or invalid) with domain, mailbox, catch-all, disposable, and role signals plus a confidence score. Malformed addresses are rejected by the input schema with no charge; every syntactically valid address returns a billed verdict, including undeliverable ones.
1425
1556
  *
1426
1557
  * Price: $0 per request plus $0.0008 per result (maximum $0.0008).
1427
1558
  *
@@ -1668,7 +1799,7 @@ var FacebookNamespace = class {
1668
1799
  /**
1669
1800
  * Facebook Marketplace
1670
1801
  *
1671
- * Search Facebook Marketplace listings by keyword near a location, with price, condition, delivery, recency, and availability filters (title, price, location, and image) as normalized JSON.
1802
+ * Search Facebook Marketplace listings by keyword near a location, filter by price, condition, delivery, recency, and availability, and get title, price, location, and image as normalized JSON.
1672
1803
  *
1673
1804
  * Price: $0.002 per request.
1674
1805
  *