@agent-cards/checkout 0.4.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1
4
+
5
+ - Stop describing every processor request rejection as a card decline or proof that nothing was charged. `ProcessorRefusedError` keeps its existing class and code, with neutral wording and an optional `processorError` containing bounded Razorpay reason, source, step and payment/order identifiers from the matching API/Vault release. Older failure records cannot recover details that were not retained.
6
+ - Hold further card requests within the attachment after a processor refusal, including when the merchant retries after the approval cooldown. Reconcile the merchant payment; only an explicit `retryAfterMerchantFailure({ status: 'failed' })` releases this hold and permits an immediate new attempt. This also applies to clear card declines. Ordinary user declines keep their approval cooldown. Do not automatically replace the attachment to bypass reconciliation.
7
+ - Add deterministic refusal, diagnostic sanitization and retry-guard regressions. These tests do not establish a successful Razorpay purchase or processor capture.
8
+
3
9
  ## 0.4.0
4
10
 
5
11
  - Add `controller.prepare({ psp: 'square', environment: 'production' | 'sandbox' })` to both browser adapters. The caller awaits cardholder consent and unlock before starting its first native Pay action. This requires the matching preparation API and Vault deployment.
package/README.md CHANGED
@@ -248,11 +248,19 @@ one. `amountAuthority` on every replay is `stripe_payment_intent`,
248
248
  refused to replay a confirm at it. An `ApprovalDeclinedError` with
249
249
  `code: 'intent_not_confirmable'`. Deliberately not "nothing was charged":
250
250
  check the intent at Stripe before retrying.
251
- - `ProcessorRefusedError`: the cardholder's device sent the card and the
252
- processor refused it outright (`pspErrorCode` is the processor's own code,
253
- such as Stripe's `card_declined`). Nothing was charged. An
254
- `ApprovalDeclinedError` with `code: 'processor_refused'`; the page's next
255
- attempt raises a fresh approval where the person can pick another card.
251
+ - `ProcessorRefusedError`: the cardholder's device reported a processor
252
+ request rejection. `pspErrorCode` carries the processor's code; optional
253
+ `processorError` carries bounded Razorpay reason, source, step and payment/order
254
+ identifiers when the API has them. A generic code such as `BAD_REQUEST_ERROR`
255
+ does not establish an issuer decline or prove no money moved. Reconcile the
256
+ merchant payment before retrying. This remains an `ApprovalDeclinedError`
257
+ with `code: 'processor_refused'` for compatibility.
258
+ The attachment records `status: 'declined', reason: 'processor_refused'`
259
+ and holds further card requests. After confirming merchant failure, call
260
+ `retryAfterMerchantFailure({ status: 'failed' })` to permit a deliberate new
261
+ attempt immediately, without waiting for the user-decline cooldown. Do not
262
+ automatically create a new attachment after this error;
263
+ the guard applies only within the existing attachment.
256
264
  - `CheckoutApiError` with `code === 'amount_unverifiable'`: Stripe could not
257
265
  be asked (502; the SDK retries twice, 500ms then 1500ms, before throwing)
258
266
  or the paused request lacked its client secret or publishable key (400).
package/dist/cdp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { BUILTIN_REGISTRY, cardUrlPatterns } from './registry.js';
2
- import { ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, PaymentOutcomeUnknownError, UnsupportedModeError, redactUrl, } from './client.js';
2
+ import { ApprovalDeclinedError, ApprovalTimeoutError, CardEncryptedError, CheckoutApiError, PaymentOutcomeUnknownError, ProcessorRefusedError, UnsupportedModeError, redactUrl, } from './client.js';
3
3
  import { PreparationGate } from './preparation.js';
4
4
  import { substituteEncryptedFields } from './substitute.js';
5
5
  import { hostedFormSubmittedPage } from './hosted-form.js';
@@ -80,6 +80,11 @@ const APPROVAL_COOLDOWN_MS = 5_000;
80
80
  * quiet window absorbs the burst; the next request past it is judged afresh.
81
81
  */
82
82
  function isApprovalOutcome(err) {
83
+ // Processor refusals are held by the lifecycle until explicit merchant
84
+ // reconciliation and retry. A second timer would outlive that release and
85
+ // reject the application's deliberate retry even after confirmed failure.
86
+ if (err instanceof ProcessorRefusedError)
87
+ return false;
83
88
  if (err instanceof CheckoutApiError)
84
89
  return err.code === 'duplicate_submission';
85
90
  return err instanceof ApprovalDeclinedError || err instanceof ApprovalTimeoutError;
@@ -98,7 +103,9 @@ function isApprovalOutcome(err) {
98
103
  * a misconfiguration or an unsupported PSP. Nothing a person does changes
99
104
  * those, so asking again is pure waste.
100
105
  *
101
- * Everything else stays retryable. A 5xx or a 429 clears on its own, and a
106
+ * Other errors may be retryable unless the lifecycle holds the attachment
107
+ * for merchant reconciliation, as it does after a processor refusal.
108
+ * A 5xx or a 429 clears on its own, and a
102
109
  * decline or a timeout is answered by the cooldown above rather than by
103
110
  * killing the page: the person said no to one authorization, not to every
104
111
  * checkout they will ever make in this session. A 409 duplicate_submission
package/dist/client.d.ts CHANGED
@@ -271,20 +271,27 @@ export declare class IntentNotConfirmableError extends ApprovalDeclinedError {
271
271
  readonly code: "intent_not_confirmable";
272
272
  constructor(authorizationId: string);
273
273
  }
274
+ /** Bounded processor identifiers, never a raw response or free-form description. */
275
+ export interface RazorpayProcessorError {
276
+ reason?: string;
277
+ source?: string;
278
+ step?: string;
279
+ payment_id?: string;
280
+ order_id?: string;
281
+ }
274
282
  /**
275
- * The cardholder's device sent the card and the processor refused it
276
- * outright (a card decline, a bad CVC, an invalid request). Nothing was
277
- * charged; the authorization is `declined` with reason `processor_refused`
278
- * and the processor's own code in `pspErrorCode` (Stripe's `card_declined`,
279
- * `incorrect_cvc`, …). A decline like any other for the adapters: the paused
280
- * request is aborted and the page's retry gets a fresh approval, where the
281
- * person can pick another card.
283
+ * The device reported a processor request rejection. A generic processor
284
+ * code does not establish an issuer decline or prove no money moved. Read
285
+ * the bounded processor evidence and reconcile the merchant before retrying.
286
+ * The authorization remains `declined` with reason `processor_refused` for
287
+ * compatibility; no successful processor response is handed to the merchant.
282
288
  */
283
289
  export declare class ProcessorRefusedError extends ApprovalDeclinedError {
284
290
  authorizationId: string;
285
291
  pspErrorCode: string | null;
286
292
  readonly code: "processor_refused";
287
- constructor(authorizationId: string, pspErrorCode: string | null);
293
+ readonly processorError: RazorpayProcessorError | null;
294
+ constructor(authorizationId: string, pspErrorCode: string | null, processorError?: RazorpayProcessorError | null);
288
295
  }
289
296
  /**
290
297
  * A non-2xx from the Agentcard API, carrying the status so callers can tell a
package/dist/client.js CHANGED
@@ -128,25 +128,49 @@ export class IntentNotConfirmableError extends ApprovalDeclinedError {
128
128
  + 'it cannot be confirmed again. Check the intent at Stripe before retrying.';
129
129
  }
130
130
  }
131
+ function parseProcessorError(value) {
132
+ if (!value || typeof value !== 'object' || Array.isArray(value))
133
+ return null;
134
+ const fields = value;
135
+ const result = {};
136
+ const exact = (pattern, field) => pattern.exec(field)?.[0] === field;
137
+ for (const [key, field] of Object.entries(fields)) {
138
+ if (typeof field !== 'string')
139
+ return null;
140
+ if (key === 'reason' || key === 'source' || key === 'step') {
141
+ if (!exact(/^[a-z][a-z_]{0,63}$/, field))
142
+ return null;
143
+ result[key] = field;
144
+ }
145
+ else if (key === 'payment_id' || key === 'order_id') {
146
+ if (!exact(key === 'payment_id' ? /^pay_[A-Za-z0-9]{1,128}$/ : /^order_[A-Za-z0-9]{1,128}$/, field))
147
+ return null;
148
+ result[key] = field;
149
+ }
150
+ else
151
+ return null;
152
+ }
153
+ return Object.keys(result).length ? result : null;
154
+ }
131
155
  /**
132
- * The cardholder's device sent the card and the processor refused it
133
- * outright (a card decline, a bad CVC, an invalid request). Nothing was
134
- * charged; the authorization is `declined` with reason `processor_refused`
135
- * and the processor's own code in `pspErrorCode` (Stripe's `card_declined`,
136
- * `incorrect_cvc`, …). A decline like any other for the adapters: the paused
137
- * request is aborted and the page's retry gets a fresh approval, where the
138
- * person can pick another card.
156
+ * The device reported a processor request rejection. A generic processor
157
+ * code does not establish an issuer decline or prove no money moved. Read
158
+ * the bounded processor evidence and reconcile the merchant before retrying.
159
+ * The authorization remains `declined` with reason `processor_refused` for
160
+ * compatibility; no successful processor response is handed to the merchant.
139
161
  */
140
162
  export class ProcessorRefusedError extends ApprovalDeclinedError {
141
163
  authorizationId;
142
164
  pspErrorCode;
143
165
  code = 'processor_refused';
144
- constructor(authorizationId, pspErrorCode) {
166
+ processorError;
167
+ constructor(authorizationId, pspErrorCode, processorError = null) {
145
168
  super('processor_refused');
146
169
  this.authorizationId = authorizationId;
147
170
  this.pspErrorCode = pspErrorCode;
148
171
  this.name = 'ProcessorRefusedError';
149
- this.message = `the processor refused the card on ${authorizationId}${pspErrorCode ? ` (${pspErrorCode})` : ''}. Nothing was charged.`;
172
+ this.processorError = parseProcessorError(processorError);
173
+ this.message = `the processor rejected the payment request on ${authorizationId}${pspErrorCode ? ` (${pspErrorCode})` : ''}. Check the merchant payment status before retrying.`;
150
174
  }
151
175
  }
152
176
  /**
@@ -611,7 +635,7 @@ export class VaultClient {
611
635
  if (s.reason === 'intent_not_confirmable')
612
636
  throw new IntentNotConfirmableError(String(created.id));
613
637
  if (s.reason === 'processor_refused') {
614
- throw new ProcessorRefusedError(String(created.id), typeof s.psp_error_code === 'string' ? s.psp_error_code : null);
638
+ throw new ProcessorRefusedError(String(created.id), typeof s.psp_error_code === 'string' ? s.psp_error_code : null, s.psp === 'razorpay' ? parseProcessorError(s.processor_error) : null);
615
639
  }
616
640
  throw new ApprovalDeclinedError(s.reason ?? 'no reason given');
617
641
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
2
- export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, } from './client.js';
2
+ export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, } from './client.js';
3
3
  export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
4
4
  export type { CdpLike, AttachOptions, CorsOutcome } from './cdp.js';
5
5
  export { substituteEncryptedFields, SubstitutionError } from './substitute.js';
package/dist/lifecycle.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ApprovalDeclinedError, ApprovalTimeoutError, CheckoutCancelledError, CheckoutPreparationError, IntentNotConfirmableError, PaymentOutcomeUnknownError } from './client.js';
1
+ import { ApprovalDeclinedError, ApprovalTimeoutError, CheckoutCancelledError, CheckoutPreparationError, IntentNotConfirmableError, PaymentOutcomeUnknownError, ProcessorRefusedError } from './client.js';
2
2
  /** Shared by the raw CDP and Playwright transports. No browser ownership or payment execution lives here. */
3
3
  export class CheckoutLifecycle {
4
4
  options;
@@ -115,7 +115,7 @@ export class CheckoutLifecycle {
115
115
  this.merchantRequestAborted();
116
116
  return;
117
117
  }
118
- const authorizationId = error instanceof PaymentOutcomeUnknownError ? error.authorizationId : this.state.authorizationId;
118
+ const authorizationId = error instanceof PaymentOutcomeUnknownError || error instanceof ProcessorRefusedError ? error.authorizationId : this.state.authorizationId;
119
119
  if (handoffStarted || error instanceof PaymentOutcomeUnknownError || error instanceof IntentNotConfirmableError) {
120
120
  this.held = true;
121
121
  this.set({ ...this.state, authorizationId, status: 'outcome_unknown', reason: error instanceof PaymentOutcomeUnknownError ? error.reason : error instanceof IntentNotConfirmableError ? 'intent_not_confirmable' : 'browser_handoff_failed' });
@@ -129,6 +129,13 @@ export class CheckoutLifecycle {
129
129
  else if (error instanceof ApprovalTimeoutError) {
130
130
  this.set({ ...this.state, status: 'timed_out', reason: 'approval_expired' });
131
131
  }
132
+ else if (error instanceof ProcessorRefusedError) {
133
+ // A rejected processor request is not proof that the merchant cannot
134
+ // collect this payment. Keep native retries held until the application
135
+ // checks the merchant and explicitly starts another attempt.
136
+ this.held = true;
137
+ this.set({ ...this.state, authorizationId, status: 'declined', reason: 'processor_refused' });
138
+ }
132
139
  else if (error instanceof ApprovalDeclinedError) {
133
140
  this.set({ ...this.state, status: 'declined', reason: 'approval_declined' });
134
141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/checkout",
3
- "version": "0.4.0",
3
+ "version": "0.4.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",