@agent-cards/checkout 0.15.0 → 0.15.1

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/dist/client.d.ts CHANGED
@@ -527,8 +527,15 @@ export declare class VaultClient {
527
527
  * exchange rather than each minting their own token.
528
528
  */
529
529
  private accessToken;
530
- /** Authenticated request that retries ONCE on a 401 with a fresh token. */
530
+ /**
531
+ * A preparation status read may retry two known connection failures, after
532
+ * 250ms and 500ms, while keeping its original approval deadline and signal.
533
+ * Only fetch and a successful response's body read belong to that retry:
534
+ * OAuth, HTTP errors, invalid JSON and every mutation remain outside it.
535
+ * The existing one-time 401 refresh carries the remaining read budget.
536
+ */
531
537
  private call;
532
538
  private post;
533
539
  private get;
540
+ private readPreparation;
534
541
  }
package/dist/client.js CHANGED
@@ -354,6 +354,17 @@ export function redactUrl(raw) {
354
354
  }
355
355
  /** Default backoff for a 502 amount_unverifiable at create: two retries, then give up. */
356
356
  const UNVERIFIABLE_RETRY_DELAYS_MS = [500, 1500];
357
+ const PREPARATION_READ_RETRY_DELAYS_MS = [250, 500];
358
+ const TRANSIENT_READ_NETWORK_CODES = new Set([
359
+ 'ECONNRESET', 'ECONNREFUSED', 'EPIPE', 'ETIMEDOUT', 'EAI_AGAIN',
360
+ 'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT',
361
+ ]);
362
+ function transientReadNetworkError(error) {
363
+ if (!(error instanceof Error) || error.name === 'AbortError' || error.name === 'TimeoutError' || error instanceof SyntaxError)
364
+ return false;
365
+ const code = error.cause?.code ?? error.code;
366
+ return typeof code === 'string' && TRANSIENT_READ_NETWORK_CODES.has(code);
367
+ }
357
368
  export class VaultClient {
358
369
  opts;
359
370
  baseUrl;
@@ -460,7 +471,7 @@ export class VaultClient {
460
471
  }
461
472
  catch { /* observer only */ }
462
473
  while (!signal.aborted) {
463
- const state = await this.get(`/v2/checkout/preparations/${id}`, signal);
474
+ const state = await this.readPreparation(id, signal);
464
475
  if (signal.aborted)
465
476
  throw fail('cancelled', id);
466
477
  if (state?.id !== id)
@@ -1023,30 +1034,49 @@ export class VaultClient {
1023
1034
  })().finally(() => { this.inflight = null; });
1024
1035
  return this.inflight;
1025
1036
  }
1026
- /** Authenticated request that retries ONCE on a 401 with a fresh token. */
1027
- async call(path, init = {}, retried = false, stopNewRequests) {
1037
+ /**
1038
+ * A preparation status read may retry two known connection failures, after
1039
+ * 250ms and 500ms, while keeping its original approval deadline and signal.
1040
+ * Only fetch and a successful response's body read belong to that retry:
1041
+ * OAuth, HTTP errors, invalid JSON and every mutation remain outside it.
1042
+ * The existing one-time 401 refresh carries the remaining read budget.
1043
+ */
1044
+ async call(path, init = {}, retried = false, stopNewRequests, preparationReadAttempt) {
1028
1045
  const signal = init.signal ?? AbortSignal.timeout(30_000);
1029
1046
  const token = await withSignal(this.accessToken(), signal);
1030
1047
  // Auth can outlive the merchant request. Stop a create that has not left
1031
1048
  // yet, while preserving the response of one already sent for cleanup.
1032
1049
  if (stopNewRequests?.aborted)
1033
1050
  throw new PaymentOutcomeUnknownError(null, 'merchant_request_aborted');
1034
- signal.throwIfAborted();
1035
- const r = await withSignal(this.fetch(`${this.baseUrl}${path}`, {
1036
- ...init,
1037
- signal,
1038
- headers: { ...(init.headers ?? {}), authorization: `Bearer ${token}`, 'content-type': 'application/json' },
1039
- }), signal);
1040
- // A token can be revoked or expire early; one forced refresh, then give up.
1041
- if (r.status === 401 && !retried) {
1042
- if (stopNewRequests?.aborted)
1043
- throw new PaymentOutcomeUnknownError(null, 'merchant_request_aborted');
1044
- await withSignal(this.accessToken(true), signal);
1045
- return this.call(path, { ...init, signal }, true, stopNewRequests);
1046
- }
1047
- if (!r.ok)
1051
+ for (;;) {
1052
+ signal.throwIfAborted();
1053
+ let r;
1054
+ try {
1055
+ r = await withSignal(this.fetch(`${this.baseUrl}${path}`, {
1056
+ ...init,
1057
+ signal,
1058
+ headers: { ...(init.headers ?? {}), authorization: `Bearer ${token}`, 'content-type': 'application/json' },
1059
+ }), signal);
1060
+ if (r.ok)
1061
+ return await withSignal(r.json(), signal);
1062
+ }
1063
+ catch (error) {
1064
+ const delay = preparationReadAttempt === undefined ? undefined : PREPARATION_READ_RETRY_DELAYS_MS[preparationReadAttempt];
1065
+ if (signal.aborted || preparationReadAttempt === undefined || delay === undefined || !transientReadNetworkError(error))
1066
+ throw error;
1067
+ preparationReadAttempt++;
1068
+ await interruptibleSleep(delay, signal);
1069
+ continue;
1070
+ }
1071
+ // A token can be revoked or expire early; one forced refresh, then give up.
1072
+ if (r.status === 401 && !retried) {
1073
+ if (stopNewRequests?.aborted)
1074
+ throw new PaymentOutcomeUnknownError(null, 'merchant_request_aborted');
1075
+ await withSignal(this.accessToken(true), signal);
1076
+ return this.call(path, { ...init, signal }, true, stopNewRequests, preparationReadAttempt);
1077
+ }
1048
1078
  throw new CheckoutApiError(r.status, path, await withSignal(r.text(), signal));
1049
- return withSignal(r.json(), signal);
1079
+ }
1050
1080
  }
1051
1081
  post(path, body, signal, stopNewRequests) {
1052
1082
  return this.call(path, { method: 'POST', body: JSON.stringify(body), signal }, false, stopNewRequests);
@@ -1054,6 +1084,9 @@ export class VaultClient {
1054
1084
  get(path, signal) {
1055
1085
  return this.call(path, { signal });
1056
1086
  }
1087
+ readPreparation(id, signal) {
1088
+ return this.call(`/v2/checkout/preparations/${id}`, { signal }, false, undefined, 0);
1089
+ }
1057
1090
  }
1058
1091
  function executionMetadata(state, authorizationId, previous = {}) {
1059
1092
  if (state.execution_mode === undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/checkout",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Let browser agents pay with the user's own card, without your infrastructure ever touching card data.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",