@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 +6 -0
- package/README.md +13 -5
- package/dist/cdp.js +9 -2
- package/dist/client.d.ts +15 -8
- package/dist/client.js +34 -10
- package/dist/index.d.ts +1 -1
- package/dist/lifecycle.js +9 -2
- package/package.json +1 -1
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
*
|
|
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
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
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
|
-
|
|
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
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
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
|
-
|
|
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.
|
|
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