@agent-cards/checkout 0.6.0 → 0.7.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/CHANGELOG.md +5 -0
- package/README.md +22 -24
- package/dist/cdp.d.ts +19 -7
- package/dist/cdp.js +67 -6
- package/dist/client.d.ts +111 -23
- package/dist/client.js +132 -20
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/preparation.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- A company can put rules on the Vault purchases it chooses (a merchant list, a currency, a spend cap, a time window) by attaching a named preset to a stored card. A purchase the rules refuse now surfaces as `PresetRefusedError`, a typed decline: at stage `create` no authorization exists and nobody was asked; at stage `pre_replay` the authorization is `declined` with the rule's reason. It carries `code`, `presetName`, `rule`, `preset`, `attachment` (the card with its last four digits) and the rule's own statement; when several presets refuse the same purchase, `refusals` names every one and the other fields are the first. The adapters quiet the page's retry as for any decline, and a create-time refusal is never treated as a permanent misconfiguration.
|
|
6
|
+
- Requires the matching API and Vault release.
|
|
7
|
+
|
|
3
8
|
## 0.6.0
|
|
4
9
|
|
|
5
10
|
- Recognize Paysafe Checkout 1.8's exact hosted tokenization endpoints and preserve its native credential and correlation headers. The matching API registry and Vault deployment are required.
|
package/README.md
CHANGED
|
@@ -54,30 +54,28 @@ await attachToCdp(cdp, pageSessionId, {
|
|
|
54
54
|
vault,
|
|
55
55
|
user: 'usr_123', // whose card should pay
|
|
56
56
|
merchant: 'vanman.shop',
|
|
57
|
-
|
|
57
|
+
amount: 583, // your hint, an integer in the currency's smallest unit (or a decimal string: '5.83')
|
|
58
58
|
currency: 'usd', // "$5.83" is derived for the approval screen
|
|
59
59
|
onApprovalUrl: (url) => sendToUser(url), // iMessage, SMS, push, your call
|
|
60
60
|
});
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`amount:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
read show one amount. Tokenization requests carry no amount, so there the
|
|
80
|
-
pair is shown and reported (`amountAuthority: 'display_only'`), not enforced.
|
|
63
|
+
`amount` is your hint: an integer in the currency's smallest unit (583 for
|
|
64
|
+
$5.83), or a decimal string in normal units ('5.83'), with `currency`. The
|
|
65
|
+
processor's own amount is the higher authority: Agentcard reads it from the
|
|
66
|
+
paused request where the processor puts it there, or from the Stripe intent
|
|
67
|
+
the request names, right before the cardholder's device replays, and the
|
|
68
|
+
company's caps are judged on it. A hint lets a bad purchase be refused the
|
|
69
|
+
moment it opens; a hint more than one smallest unit away from the processor's
|
|
70
|
+
amount is refused with nothing charged (`AmountMismatchError`), and after the
|
|
71
|
+
replay the charge is reconciled against the approval
|
|
72
|
+
(`ReplayResponse.amountVerified`, `chargedAmount`, and `chargedKind`:
|
|
73
|
+
`captured` for a succeeded intent's `amount_received`, `authorized` for a
|
|
74
|
+
manual-capture intent's `amount_capturable`, `none` when nothing is collected
|
|
75
|
+
yet; plus the `checkout_authorization.amount_mismatch` webhook to your server
|
|
76
|
+
when the charge disagreed). Every result carries `amountAuthority`:
|
|
77
|
+
`processor`, `agent`, `page`, or `none`. A display string is never sent;
|
|
78
|
+
Agentcard derives it.
|
|
81
79
|
|
|
82
80
|
Then let your agent click "Pay" like it always does. `attachToCdp` pauses the
|
|
83
81
|
request for approval and resumes it only while the merchant request remains
|
|
@@ -129,7 +127,7 @@ Coverage is specific to the processor request format, merchant setup, browser tr
|
|
|
129
127
|
| Processor | Status |
|
|
130
128
|
|---|---|
|
|
131
129
|
| Shopify | supported, verified end to end |
|
|
132
|
-
| Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms
|
|
130
|
+
| Stripe | tokenization replay and direct card-bearing PaymentIntent confirms are implemented; direct confirms read the intent's amount back from Stripe; a hint sent as `amount` + `currency` must agree with it. Browser token-to-intent continuation is unsupported and held. Validate the exact merchant flow before pilot use |
|
|
133
131
|
| Braintree card tokenization | Prepared checkout supported; one live Haymarket Books ebook purchase with SDK `0.5.0` confirmed merchant fulfillment and SDK `completed` using a merchant receipt resolver. Independent processor capture/settlement, live 3DS and PayPal wallet flows remain unverified. |
|
|
134
132
|
| Checkout.com | supported |
|
|
135
133
|
| VGS Collect (Very Good Security; Wolt) | not supported: VGS's proxy aliases only submissions from its own iframe, so a replay from the cardholder's device is refused by the merchant (verified on Wolt, 2026-09-03). Not recognized, so the agent's browser is not paused there |
|
|
@@ -352,7 +350,7 @@ value continues to work. Choose `requireMerchantResult: true` for a pilot:
|
|
|
352
350
|
|
|
353
351
|
```ts
|
|
354
352
|
const checkout = await attachToPlaywright(page, {
|
|
355
|
-
vault, user, merchant,
|
|
353
|
+
vault, user, merchant, amount, currency,
|
|
356
354
|
requireMerchantResult: true,
|
|
357
355
|
onStateChange: state => recordState(state),
|
|
358
356
|
onUserAction: action => deliverPrivatelyToUser(action),
|
|
@@ -395,7 +393,7 @@ Unrecognized merchant-server endpoints remain outside this guard unless listed
|
|
|
395
393
|
in `paymentEndpoints`; this is not a guarantee against a merchant charging a
|
|
396
394
|
saved token on its own server.
|
|
397
395
|
A direct card-bearing PaymentIntent confirm remains supported with the backend's
|
|
398
|
-
existing amount verification when `
|
|
396
|
+
existing amount verification when `amount` and `currency` are supplied.
|
|
399
397
|
|
|
400
398
|
Hosted-form submissions also always stay blocked because their payment outcome
|
|
401
399
|
is unverified. `reconcile()` calls the resolver once, coalescing concurrent calls.
|
|
@@ -417,7 +415,7 @@ Prepare a Square, Braintree, Worldpay, Bambora or Mercado Pago checkout before t
|
|
|
417
415
|
```ts
|
|
418
416
|
const checkout = await attachToPlaywright(page, {
|
|
419
417
|
vault, user: 'your-user-id', merchant: 'Example merchant',
|
|
420
|
-
|
|
418
|
+
amount: 100, currency: 'USD',
|
|
421
419
|
onApprovalUrl: deliverPrivatelyToCardholder,
|
|
422
420
|
});
|
|
423
421
|
const preparation = await checkout.prepare({
|
|
@@ -429,7 +427,7 @@ const preparation = await checkout.prepare({
|
|
|
429
427
|
await page.getByRole('button', { name: 'Pay', exact: true }).click();
|
|
430
428
|
```
|
|
431
429
|
|
|
432
|
-
`prepare()` is available on both Playwright and raw CDP controllers. It requires `
|
|
430
|
+
`prepare()` is available on both Playwright and raw CDP controllers. It requires `amount` and `currency`, must precede the first recognized card request, and returns only when the cardholder's device is ready. It delivers the preparation URL through `onApprovalUrl` and `onUserAction`; binding the subsequent authorization sends no second approval link or SMS. The phone page must stay open. Its selected card, merchant origin, declared merchant, amount, currency, processor and environment bind one fresh request. The amount's authority is `agent`; a card token does not enforce the merchant's eventual charge amount.
|
|
433
431
|
|
|
434
432
|
| Processor | `environment` | Fresh native request |
|
|
435
433
|
| --- | --- | --- |
|
package/dist/cdp.d.ts
CHANGED
|
@@ -75,16 +75,28 @@ export interface AttachOptions extends LifecycleOptions {
|
|
|
75
75
|
vault: VaultClient;
|
|
76
76
|
user: string;
|
|
77
77
|
merchant: string;
|
|
78
|
-
/** Display string for the approval screen. Optional when amountCents + currency are given. */
|
|
79
|
-
amount?: string;
|
|
80
78
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* right before
|
|
79
|
+
* Your hint at the amount, an integer in the currency's smallest unit or a
|
|
80
|
+
* decimal string in normal units, with its ISO 4217 code. See
|
|
81
|
+
* AuthorizeInput.amount: the processor's own amount is the higher authority
|
|
82
|
+
* and is read right before the card is sent; a hint lets a bad purchase be
|
|
83
|
+
* refused the moment it opens, and one that disagrees with the processor is
|
|
84
|
+
* refused with nothing charged.
|
|
85
85
|
*/
|
|
86
|
-
|
|
86
|
+
amount?: number | string;
|
|
87
87
|
currency?: string;
|
|
88
|
+
/**
|
|
89
|
+
* The total the checkout page shows, the lowest authority: used only when
|
|
90
|
+
* neither the processor's request nor your `amount` names one. Pass a reader
|
|
91
|
+
* that returns an integer in the currency's smallest unit with its code
|
|
92
|
+
* (`{ amount: 4210, currency: 'usd' }`), read off the page however your
|
|
93
|
+
* page spells it; the adapters call it when a card request pauses, give it
|
|
94
|
+
* one second, and send nothing when it yields nothing. Absent: no page total.
|
|
95
|
+
*/
|
|
96
|
+
pageAmount?: () => Promise<{
|
|
97
|
+
amount: number;
|
|
98
|
+
currency: string;
|
|
99
|
+
} | undefined>;
|
|
88
100
|
cardId?: string;
|
|
89
101
|
timeoutMs?: number;
|
|
90
102
|
/** Browser interception setup deadline, separate from approval. Defaults to 30000 ms; maximum 300000 ms. */
|
package/dist/cdp.js
CHANGED
|
@@ -274,6 +274,59 @@ export function withCorsHeaders(headers, cors) {
|
|
|
274
274
|
function headerEntries(headers) {
|
|
275
275
|
return Object.entries(headers).map(([name, value]) => ({ name, value: String(value) }));
|
|
276
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* The origin of the top-level document the payment form is on, read when a
|
|
279
|
+
* card request pauses: the fact that names the merchant for the company's
|
|
280
|
+
* presets (the API's checkout_origin). An https origin, or http://localhost
|
|
281
|
+
* for local checkout; anything else, or a page that cannot be read, yields
|
|
282
|
+
* undefined and the authorization goes out without one (a merchant, category
|
|
283
|
+
* or place rule then refuses it as unknown, never the request itself).
|
|
284
|
+
*/
|
|
285
|
+
async function pageOriginOf(readDocumentUrl) {
|
|
286
|
+
try {
|
|
287
|
+
const url = new URL(await readDocumentUrl());
|
|
288
|
+
if (url.username || url.password)
|
|
289
|
+
return undefined;
|
|
290
|
+
if (url.protocol === 'https:' || (url.protocol === 'http:' && url.hostname === 'localhost'))
|
|
291
|
+
return url.origin;
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The page total, the lowest amount authority, through the integrator's own
|
|
300
|
+
* reader (AttachOptions.pageAmount): an integer in the smallest unit with its
|
|
301
|
+
* currency, or nothing. A read is a courtesy, never a wait: it gets one
|
|
302
|
+
* second, and anything unreadable yields undefined so the authorization goes
|
|
303
|
+
* out without a page total.
|
|
304
|
+
*/
|
|
305
|
+
const PAGE_AMOUNT_READ_MS = 1_000;
|
|
306
|
+
async function pageAmountOf(opts) {
|
|
307
|
+
if (!opts.pageAmount)
|
|
308
|
+
return undefined;
|
|
309
|
+
let timer;
|
|
310
|
+
try {
|
|
311
|
+
const raw = await Promise.race([
|
|
312
|
+
Promise.resolve().then(() => opts.pageAmount()).catch(() => undefined),
|
|
313
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(undefined), PAGE_AMOUNT_READ_MS); }),
|
|
314
|
+
]);
|
|
315
|
+
if (!raw || typeof raw !== 'object')
|
|
316
|
+
return undefined;
|
|
317
|
+
const { amount, currency } = raw;
|
|
318
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || typeof currency !== 'string' || !/^[A-Za-z]{3}$/.test(currency))
|
|
319
|
+
return undefined;
|
|
320
|
+
return { amount: amount, currency: currency.toLowerCase() };
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
if (timer)
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
277
330
|
/**
|
|
278
331
|
* Last-resort patterns: the built-in recognizers' hosts, derived the same way
|
|
279
332
|
* as everything else. Used only when the vault hands back nothing at all (a
|
|
@@ -335,13 +388,14 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
335
388
|
let setupFailureReported = false;
|
|
336
389
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
337
390
|
let preparationFrameId;
|
|
338
|
-
const
|
|
391
|
+
const readDocumentUrl = async () => {
|
|
339
392
|
const tree = await cdp.send('Page.getFrameTree', {}, pageSessionId);
|
|
340
393
|
if (typeof tree?.frameTree?.frame?.url !== 'string')
|
|
341
394
|
throw new Error('merchant_document_unavailable');
|
|
342
395
|
preparationFrameId = tree.frameTree.frame.id;
|
|
343
396
|
return tree.frameTree.frame.url;
|
|
344
|
-
}
|
|
397
|
+
};
|
|
398
|
+
const preparationGate = new PreparationGate(opts, lifecycle, readDocumentUrl);
|
|
345
399
|
const guards = paymentEndpointGuards(opts.paymentEndpoints);
|
|
346
400
|
const armed = new Set();
|
|
347
401
|
const arming = new Map();
|
|
@@ -533,14 +587,17 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
533
587
|
activeRequest = { attempt, networkId, sessionId, frameId };
|
|
534
588
|
if (preparation)
|
|
535
589
|
await preparationGate.assertDocument();
|
|
590
|
+
const pageOrigin = await pageOriginOf(readDocumentUrl);
|
|
591
|
+
const pageAmount = await pageAmountOf(opts);
|
|
536
592
|
attempt.assertLive();
|
|
537
593
|
const replay = await opts.vault.authorize({
|
|
538
594
|
user: opts.user,
|
|
539
595
|
merchant: opts.merchant,
|
|
540
596
|
amount: opts.amount,
|
|
541
|
-
amountCents: opts.amountCents,
|
|
542
597
|
currency: opts.currency,
|
|
543
598
|
cardId: preparation?.cardId ?? opts.cardId,
|
|
599
|
+
pageOrigin,
|
|
600
|
+
pageAmount,
|
|
544
601
|
preparation,
|
|
545
602
|
timeoutMs: opts.timeoutMs,
|
|
546
603
|
signal: lifecycle.abort.signal,
|
|
@@ -675,11 +732,12 @@ export async function attachToPlaywright(page, opts) {
|
|
|
675
732
|
throw new Error('Service workers are active; use a checkout context created with serviceWorkers: "block".');
|
|
676
733
|
}
|
|
677
734
|
const lifecycle = new CheckoutLifecycle(opts);
|
|
678
|
-
const
|
|
735
|
+
const readDocumentUrl = async () => {
|
|
679
736
|
if (page.isClosed?.() || typeof page.url !== 'function')
|
|
680
737
|
throw new Error('merchant_document_unavailable');
|
|
681
738
|
return page.url();
|
|
682
|
-
}
|
|
739
|
+
};
|
|
740
|
+
const preparationGate = new PreparationGate(opts, lifecycle, readDocumentUrl);
|
|
683
741
|
const guards = paymentEndpointGuards(opts.paymentEndpoints);
|
|
684
742
|
// Playwright's own routing, NOT a hand-rolled CDP session.
|
|
685
743
|
//
|
|
@@ -800,14 +858,17 @@ export async function attachToPlaywright(page, opts) {
|
|
|
800
858
|
activeRequest = { request, frames, attempt };
|
|
801
859
|
if (preparation)
|
|
802
860
|
await preparationGate.assertDocument();
|
|
861
|
+
const pageOrigin = await pageOriginOf(readDocumentUrl);
|
|
862
|
+
const pageAmount = await pageAmountOf(opts);
|
|
803
863
|
assertRequestLive();
|
|
804
864
|
const replay = await opts.vault.authorize({
|
|
805
865
|
user: opts.user,
|
|
806
866
|
merchant: opts.merchant,
|
|
807
867
|
amount: opts.amount,
|
|
808
|
-
amountCents: opts.amountCents,
|
|
809
868
|
currency: opts.currency,
|
|
810
869
|
cardId: preparation?.cardId ?? opts.cardId,
|
|
870
|
+
pageOrigin,
|
|
871
|
+
pageAmount,
|
|
811
872
|
preparation,
|
|
812
873
|
timeoutMs: opts.timeoutMs,
|
|
813
874
|
signal: lifecycle.abort.signal,
|
package/dist/client.d.ts
CHANGED
|
@@ -19,7 +19,16 @@ export declare const SUPPORTED_MODES: readonly CheckoutMode[];
|
|
|
19
19
|
* (hosted_form: the bytes the device submits name the amount), or a display
|
|
20
20
|
* fact on a template that carries no amount.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Who named the amount an authorization carries: `processor` (the paused
|
|
24
|
+
* request's own bytes, or the Stripe intent it names, read back by Agentcard),
|
|
25
|
+
* `agent` (the `amount` you passed), `page` (the total read off the checkout
|
|
26
|
+
* page), or `none` (nobody yet; the processor is read right before the card
|
|
27
|
+
* is sent).
|
|
28
|
+
*/
|
|
29
|
+
export type AmountAuthority = 'processor' | 'agent' | 'page' | 'none';
|
|
30
|
+
/** An integer in the smallest unit, or a decimal string in normal units with a point; nothing else. */
|
|
31
|
+
export declare function validAmountInput(amount: unknown): amount is number | string;
|
|
23
32
|
/**
|
|
24
33
|
* The token flow: the cardholder's device called the processor itself, and
|
|
25
34
|
* this is the processor's answer to replay into the paused request.
|
|
@@ -41,17 +50,18 @@ export interface TokenReplay {
|
|
|
41
50
|
* moved) and Agentcard also sent your server checkout_authorization.amount_mismatch.
|
|
42
51
|
*/
|
|
43
52
|
amountVerified?: boolean | null;
|
|
44
|
-
|
|
53
|
+
/** What the processor collected, an integer in the currency's smallest unit (Stripe's `amount`). */
|
|
54
|
+
chargedAmount?: number | null;
|
|
45
55
|
chargedCurrency?: string | null;
|
|
46
56
|
/**
|
|
47
|
-
* What `
|
|
57
|
+
* What `chargedAmount` is: 'captured' (the intent succeeded; this is
|
|
48
58
|
* amount_received), 'authorized' (requires_capture; amount_capturable, the
|
|
49
59
|
* merchant captures later), 'none' (a PaymentIntent was reported but
|
|
50
60
|
* nothing is collected yet: processing, requires_action), or null when no
|
|
51
61
|
* PaymentIntent was reported at all (a tokenization request).
|
|
52
62
|
*/
|
|
53
63
|
chargedKind?: 'captured' | 'authorized' | 'none' | null;
|
|
54
|
-
/**
|
|
64
|
+
/** Who named the amount the person approved. */
|
|
55
65
|
amountAuthority?: AmountAuthority;
|
|
56
66
|
}
|
|
57
67
|
/**
|
|
@@ -108,7 +118,8 @@ export interface PrepareCheckoutOptions {
|
|
|
108
118
|
export interface PrepareCheckoutInput extends PrepareCheckoutOptions {
|
|
109
119
|
user: string;
|
|
110
120
|
merchant: string;
|
|
111
|
-
|
|
121
|
+
/** An integer in the currency's smallest unit (2306 for $23.06), or a decimal string in normal units ("23.06"). */
|
|
122
|
+
amount: number | string;
|
|
112
123
|
currency: string;
|
|
113
124
|
cardId?: string;
|
|
114
125
|
merchantOrigin: string;
|
|
@@ -127,12 +138,14 @@ export interface PreparedCheckout {
|
|
|
127
138
|
readonly cardId: string;
|
|
128
139
|
readonly user: string;
|
|
129
140
|
readonly merchant: string;
|
|
130
|
-
|
|
141
|
+
/** The approved amount, an integer in the currency's smallest unit, as the API confirmed it. */
|
|
142
|
+
readonly amount: number;
|
|
143
|
+
readonly amountDisplay: string | null;
|
|
131
144
|
readonly currency: string;
|
|
132
145
|
readonly merchantOrigin: string;
|
|
133
146
|
readonly checkoutKey: string;
|
|
134
147
|
readonly paymentStatus: 'not_started';
|
|
135
|
-
readonly amountAuthority: '
|
|
148
|
+
readonly amountAuthority: 'agent';
|
|
136
149
|
}
|
|
137
150
|
export declare class CheckoutPreparationError extends Error {
|
|
138
151
|
preparationId: string | null;
|
|
@@ -142,27 +155,31 @@ export declare class CheckoutPreparationError extends Error {
|
|
|
142
155
|
export interface AuthorizeInput {
|
|
143
156
|
/** Your identifier for the person whose card should pay. */
|
|
144
157
|
user: string;
|
|
145
|
-
/** Shown to the user on the approval screen. */
|
|
158
|
+
/** Shown to the user on the approval screen. Judged by nothing: the merchant comes from the checkout page. */
|
|
146
159
|
merchant: string;
|
|
147
160
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
161
|
+
* Your hint at the amount: an integer in the currency's smallest unit (2306
|
|
162
|
+
* for $23.06), or a decimal string in normal units ("23.06"), with its ISO
|
|
163
|
+
* 4217 code ("usd"). Optional: the processor's own amount is the higher
|
|
164
|
+
* authority, read from the paused request or from the Stripe intent it
|
|
165
|
+
* names right before the cardholder's device replays, and the company's
|
|
166
|
+
* caps are judged on it. A hint lets a bad purchase be refused the moment
|
|
167
|
+
* you open it, and a hint that disagrees with the processor beyond one
|
|
168
|
+
* smallest unit is refused with nothing charged (an AmountMismatchError;
|
|
169
|
+
* `stage` says which check). Agentcard derives the display string; you
|
|
170
|
+
* never send one. Both or neither: one without the other is refused.
|
|
151
171
|
*/
|
|
152
|
-
amount?: string;
|
|
172
|
+
amount?: number | string;
|
|
173
|
+
currency?: string;
|
|
153
174
|
/**
|
|
154
|
-
* The
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* at create and again right before the cardholder's device replays, and a
|
|
158
|
-
* different amount is refused with nothing charged (an AmountMismatchError
|
|
159
|
-
* either way: `stage` says which check). After the replay the charge is
|
|
160
|
-
* reconciled against the approval (see ReplayResponse.amountVerified).
|
|
161
|
-
* Tokenization requests carry no amount, so there it is display-only.
|
|
162
|
-
* Both or neither: one without the other is refused.
|
|
175
|
+
* The total read off the checkout page, the lowest authority: used only
|
|
176
|
+
* when neither the processor's request nor your hint names an amount. The
|
|
177
|
+
* adapters fill it from `[data-agentcard-amount]` when a page carries one.
|
|
163
178
|
*/
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
pageAmount?: {
|
|
180
|
+
amount: number;
|
|
181
|
+
currency: string;
|
|
182
|
+
};
|
|
166
183
|
/**
|
|
167
184
|
* WHICH stored card should pay — a vault card id from
|
|
168
185
|
* GET /api/v2/vault_cards. The approval page preselects it (the human can
|
|
@@ -171,6 +188,14 @@ export interface AuthorizeInput {
|
|
|
171
188
|
* `card_not_found`.
|
|
172
189
|
*/
|
|
173
190
|
cardId?: string;
|
|
191
|
+
/**
|
|
192
|
+
* The origin of the checkout page the payment form was on
|
|
193
|
+
* (https://shop.example.com). The adapters read it from the page; pass it
|
|
194
|
+
* yourself when you run your own interception. With the processor identity
|
|
195
|
+
* in the paused request this is what names the merchant for the company's
|
|
196
|
+
* presets; `merchant` above is shown to the person and judged by nothing.
|
|
197
|
+
*/
|
|
198
|
+
pageOrigin?: string;
|
|
174
199
|
request: PausedRequest;
|
|
175
200
|
/** Abort if the user has not approved within this many ms. Default 15 min. */
|
|
176
201
|
timeoutMs?: number;
|
|
@@ -300,6 +325,69 @@ export declare class ProcessorRefusedError extends ApprovalDeclinedError {
|
|
|
300
325
|
* retrying is worth anything: a 404 `connection_not_found` will answer the same
|
|
301
326
|
* way forever, while a 429 or a 502 will not.
|
|
302
327
|
*/
|
|
328
|
+
/**
|
|
329
|
+
* The company's preset refused the purchase. The company that runs this
|
|
330
|
+
* integration put rules on its users' Vault purchases (a merchant list, a
|
|
331
|
+
* currency, a spend cap, a time window); this purchase is outside them.
|
|
332
|
+
* Nothing was charged. Two stages:
|
|
333
|
+
* - 'create': refused before any authorization existed (`authorizationId`
|
|
334
|
+
* is null); nobody was asked to approve. Per purchase, not per page: the
|
|
335
|
+
* next request on the same page is judged afresh.
|
|
336
|
+
* - 'pre_replay': refused right before the cardholder's device would have
|
|
337
|
+
* sent the card; the authorization is `declined` with `code` as reason.
|
|
338
|
+
* `code` is the rule's reason (merchant_denied, currency_denied,
|
|
339
|
+
* spend_rate_exceeded, time_window_denied, ...), `message` the rule's own
|
|
340
|
+
* statement with the company's next step, `preset` the version that judged
|
|
341
|
+
* it. When several presets refused the same purchase, `code`, `preset`,
|
|
342
|
+
* `rule` and `attachment` are the first, and `refusals` names every one. A
|
|
343
|
+
* decline in every structural sense (the adapters quiet the page's retry as
|
|
344
|
+
* for a person's "no"), so it extends ApprovalDeclinedError.
|
|
345
|
+
*/
|
|
346
|
+
export declare class PresetRefusedError extends ApprovalDeclinedError {
|
|
347
|
+
readonly authorizationId: string | null;
|
|
348
|
+
readonly code: string;
|
|
349
|
+
readonly detail: string | null;
|
|
350
|
+
readonly preset: {
|
|
351
|
+
id: string;
|
|
352
|
+
version: number;
|
|
353
|
+
name: string;
|
|
354
|
+
} | null;
|
|
355
|
+
readonly rule: string | null;
|
|
356
|
+
readonly stage: 'create' | 'pre_replay';
|
|
357
|
+
/** The stored card the preset that refused is attached to, with its last four digits. */
|
|
358
|
+
readonly attachment: PresetAttachment | null;
|
|
359
|
+
/** The preset's name. */
|
|
360
|
+
readonly presetName: string | null;
|
|
361
|
+
/** Every preset that refused, each with its attachment, rule, code and statement; one entry when one refused. */
|
|
362
|
+
readonly refusals: readonly PresetRefusal[];
|
|
363
|
+
constructor(authorizationId: string | null, code: string, detail: string | null, preset: {
|
|
364
|
+
id: string;
|
|
365
|
+
version: number;
|
|
366
|
+
name: string;
|
|
367
|
+
} | null, rule: string | null, stage: 'create' | 'pre_replay',
|
|
368
|
+
/** The stored card the preset that refused is attached to, with its last four digits. */
|
|
369
|
+
attachment?: PresetAttachment | null, refusals?: readonly PresetRefusal[]);
|
|
370
|
+
}
|
|
371
|
+
/** The stored card a preset is attached to, with its last four digits. */
|
|
372
|
+
export interface PresetAttachment {
|
|
373
|
+
kind: 'card';
|
|
374
|
+
targetId: string | null;
|
|
375
|
+
last4: string | null;
|
|
376
|
+
}
|
|
377
|
+
/** One preset's refusal of a purchase. */
|
|
378
|
+
export interface PresetRefusal {
|
|
379
|
+
preset: {
|
|
380
|
+
id: string;
|
|
381
|
+
version: number;
|
|
382
|
+
name: string;
|
|
383
|
+
};
|
|
384
|
+
attachment: PresetAttachment | null;
|
|
385
|
+
rule: string | null;
|
|
386
|
+
code: string;
|
|
387
|
+
detail: string | null;
|
|
388
|
+
}
|
|
389
|
+
/** A decline reason only the company presets stamp (see PresetRefusedError). */
|
|
390
|
+
export declare const PRESET_REFUSAL_REASONS: ReadonlySet<string>;
|
|
303
391
|
export declare class CheckoutApiError extends Error {
|
|
304
392
|
status: number;
|
|
305
393
|
path: string;
|
package/dist/client.js
CHANGED
|
@@ -6,7 +6,13 @@ import { matchesPreparedRequest, validPreparationEnvironment } from './prepared-
|
|
|
6
6
|
* is never paused) and sent on every create.
|
|
7
7
|
*/
|
|
8
8
|
export const SUPPORTED_MODES = ['token', 'cse', 'hosted_form'];
|
|
9
|
-
|
|
9
|
+
/** An integer in the smallest unit, or a decimal string in normal units with a point; nothing else. */
|
|
10
|
+
export function validAmountInput(amount) {
|
|
11
|
+
if (typeof amount === 'number')
|
|
12
|
+
return Number.isSafeInteger(amount) && amount >= 0;
|
|
13
|
+
return typeof amount === 'string' && /^\d{1,15}\.\d{1,3}$/.test(amount.trim());
|
|
14
|
+
}
|
|
15
|
+
const AMOUNT_AUTHORITIES = ['processor', 'agent', 'page', 'none'];
|
|
10
16
|
export class CheckoutPreparationError extends Error {
|
|
11
17
|
preparationId;
|
|
12
18
|
reason;
|
|
@@ -180,6 +186,94 @@ export class ProcessorRefusedError extends ApprovalDeclinedError {
|
|
|
180
186
|
* retrying is worth anything: a 404 `connection_not_found` will answer the same
|
|
181
187
|
* way forever, while a 429 or a 502 will not.
|
|
182
188
|
*/
|
|
189
|
+
/**
|
|
190
|
+
* The company's preset refused the purchase. The company that runs this
|
|
191
|
+
* integration put rules on its users' Vault purchases (a merchant list, a
|
|
192
|
+
* currency, a spend cap, a time window); this purchase is outside them.
|
|
193
|
+
* Nothing was charged. Two stages:
|
|
194
|
+
* - 'create': refused before any authorization existed (`authorizationId`
|
|
195
|
+
* is null); nobody was asked to approve. Per purchase, not per page: the
|
|
196
|
+
* next request on the same page is judged afresh.
|
|
197
|
+
* - 'pre_replay': refused right before the cardholder's device would have
|
|
198
|
+
* sent the card; the authorization is `declined` with `code` as reason.
|
|
199
|
+
* `code` is the rule's reason (merchant_denied, currency_denied,
|
|
200
|
+
* spend_rate_exceeded, time_window_denied, ...), `message` the rule's own
|
|
201
|
+
* statement with the company's next step, `preset` the version that judged
|
|
202
|
+
* it. When several presets refused the same purchase, `code`, `preset`,
|
|
203
|
+
* `rule` and `attachment` are the first, and `refusals` names every one. A
|
|
204
|
+
* decline in every structural sense (the adapters quiet the page's retry as
|
|
205
|
+
* for a person's "no"), so it extends ApprovalDeclinedError.
|
|
206
|
+
*/
|
|
207
|
+
export class PresetRefusedError extends ApprovalDeclinedError {
|
|
208
|
+
authorizationId;
|
|
209
|
+
code;
|
|
210
|
+
detail;
|
|
211
|
+
preset;
|
|
212
|
+
rule;
|
|
213
|
+
stage;
|
|
214
|
+
attachment;
|
|
215
|
+
/** The preset's name. */
|
|
216
|
+
presetName;
|
|
217
|
+
/** Every preset that refused, each with its attachment, rule, code and statement; one entry when one refused. */
|
|
218
|
+
refusals;
|
|
219
|
+
constructor(authorizationId, code, detail, preset, rule, stage,
|
|
220
|
+
/** The stored card the preset that refused is attached to, with its last four digits. */
|
|
221
|
+
attachment = null, refusals = []) {
|
|
222
|
+
super(code);
|
|
223
|
+
this.authorizationId = authorizationId;
|
|
224
|
+
this.code = code;
|
|
225
|
+
this.detail = detail;
|
|
226
|
+
this.preset = preset;
|
|
227
|
+
this.rule = rule;
|
|
228
|
+
this.stage = stage;
|
|
229
|
+
this.attachment = attachment;
|
|
230
|
+
this.name = 'PresetRefusedError';
|
|
231
|
+
this.presetName = preset?.name ?? null;
|
|
232
|
+
this.refusals = refusals.length ? refusals : preset ? [{ preset, attachment, rule, code, detail }] : [];
|
|
233
|
+
const others = this.refusals.slice(1).map((r) => `"${r.preset.name}"`);
|
|
234
|
+
const also = others.length ? ` Also refused by ${others.length === 1 ? 'preset' : 'presets'} ${others.join(', ')}.` : '';
|
|
235
|
+
this.message = `refused by the company preset${preset ? ` "${preset.name}"` : ''} (${code}${stage === 'create' ? ', before any authorization was created' : ''}): ${detail ?? 'this purchase is outside the rules the company set'}${also}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** A decline reason only the company presets stamp (see PresetRefusedError). */
|
|
239
|
+
export const PRESET_REFUSAL_REASONS = new Set([
|
|
240
|
+
'spend_total_exceeded', 'spend_rate_exceeded', 'spend_rate_unknown',
|
|
241
|
+
'category_denied', 'category_unknown', 'merchant_denied', 'merchant_unknown',
|
|
242
|
+
'geo_denied', 'geo_unknown', 'currency_denied', 'currency_unknown',
|
|
243
|
+
'time_window_denied', 'surface_denied', 'surface_unknown',
|
|
244
|
+
]);
|
|
245
|
+
function presetOf(v) {
|
|
246
|
+
if (!v || typeof v !== 'object')
|
|
247
|
+
return null;
|
|
248
|
+
const p = v;
|
|
249
|
+
if (typeof p.id !== 'string' || typeof p.version !== 'number' || typeof p.name !== 'string')
|
|
250
|
+
return null;
|
|
251
|
+
return { id: p.id, version: p.version, name: p.name };
|
|
252
|
+
}
|
|
253
|
+
function attachmentOf(v) {
|
|
254
|
+
if (!v || typeof v !== 'object')
|
|
255
|
+
return null;
|
|
256
|
+
const a = v;
|
|
257
|
+
if (a.kind !== 'card')
|
|
258
|
+
return null;
|
|
259
|
+
return { kind: a.kind, targetId: typeof a.target_id === 'string' ? a.target_id : null, last4: typeof a.last4 === 'string' ? a.last4 : null };
|
|
260
|
+
}
|
|
261
|
+
/** The `refusals` list of a refusal envelope: every entry that names a preset. */
|
|
262
|
+
function refusalsOf(v) {
|
|
263
|
+
if (!Array.isArray(v))
|
|
264
|
+
return [];
|
|
265
|
+
const out = [];
|
|
266
|
+
for (const item of v) {
|
|
267
|
+
if (!item || typeof item !== 'object')
|
|
268
|
+
continue;
|
|
269
|
+
const r = item;
|
|
270
|
+
const preset = presetOf(r.preset);
|
|
271
|
+
if (!preset || typeof r.reason !== 'string')
|
|
272
|
+
continue;
|
|
273
|
+
out.push({ preset, attachment: attachmentOf(r.attachment), rule: typeof r.rule === 'string' ? r.rule : null, code: r.reason, detail: typeof r.message === 'string' ? r.message : null });
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
183
277
|
export class CheckoutApiError extends Error {
|
|
184
278
|
status;
|
|
185
279
|
path;
|
|
@@ -218,6 +312,12 @@ export class CheckoutApiError extends Error {
|
|
|
218
312
|
get permanent() {
|
|
219
313
|
if (this.code === 'amount_mismatch' || this.code === 'duplicate_submission')
|
|
220
314
|
return false;
|
|
315
|
+
// Nor a refusal by the company preset (a 403 carrying `preset` in the
|
|
316
|
+
// envelope): the company's rules refused THIS purchase, at this merchant,
|
|
317
|
+
// for this amount, at this hour. The next one on the same page may pass,
|
|
318
|
+
// so the page is quieted like a decline, never latched.
|
|
319
|
+
if (this.details.preset && typeof this.details.preset === 'object')
|
|
320
|
+
return false;
|
|
221
321
|
return this.status >= 400 && this.status < 500 && this.status !== 429;
|
|
222
322
|
}
|
|
223
323
|
}
|
|
@@ -317,7 +417,7 @@ export class VaultClient {
|
|
|
317
417
|
const fail = (reason, id = null) => new CheckoutPreparationError(id, reason);
|
|
318
418
|
if (!validPreparationEnvironment(input.psp, input.environment))
|
|
319
419
|
throw fail('unsupported_processor');
|
|
320
|
-
if (!
|
|
420
|
+
if (!validAmountInput(input.amount) || typeof input.currency !== 'string' || !/^[a-z]{3}$/i.test(input.currency))
|
|
321
421
|
throw fail('amount_required');
|
|
322
422
|
const origin = new URL(input.merchantOrigin);
|
|
323
423
|
if (!(origin.protocol === 'https:' || (origin.protocol === 'http:' && origin.hostname === 'localhost')) || origin.origin !== input.merchantOrigin)
|
|
@@ -336,7 +436,7 @@ export class VaultClient {
|
|
|
336
436
|
// Drain a sent creation even after caller cancellation to retire its ID.
|
|
337
437
|
// The separate stop signal prevents dispatch after a slow OAuth exchange.
|
|
338
438
|
const created = await this.post('/v2/checkout/preparations', {
|
|
339
|
-
user: input.user, merchant: input.merchant,
|
|
439
|
+
user: input.user, merchant: input.merchant, amount: input.amount, currency: input.currency.toLowerCase(),
|
|
340
440
|
...(input.cardId ? { card_id: input.cardId } : {}), psp: input.psp, mode: 'token',
|
|
341
441
|
environment: input.environment, checkout_key: input.checkoutKey, merchant_origin: input.merchantOrigin,
|
|
342
442
|
}, AbortSignal.timeout(30_000), signal);
|
|
@@ -365,17 +465,18 @@ export class VaultClient {
|
|
|
365
465
|
if (state.status === 'ready') {
|
|
366
466
|
const expiry = Date.parse(state.ready_expires_at);
|
|
367
467
|
if (!Number.isFinite(expiry) || expiry <= Date.now() || typeof state.card_id !== 'string' || !state.card_id
|
|
368
|
-
|| state.payment_status !== 'not_started' || state.amount_authority !== '
|
|
468
|
+
|| state.payment_status !== 'not_started' || state.amount_authority !== 'agent'
|
|
369
469
|
|| state.user !== input.user || state.merchant !== input.merchant || state.merchant_origin !== input.merchantOrigin
|
|
370
|
-
|| state.
|
|
470
|
+
|| !Number.isSafeInteger(state.amount) || (typeof input.amount === 'number' && state.amount !== input.amount) || state.currency !== input.currency.toLowerCase()
|
|
371
471
|
|| state.psp !== input.psp || state.mode !== 'token' || state.environment !== input.environment
|
|
372
472
|
|| state.checkout_key !== input.checkoutKey)
|
|
373
473
|
throw fail('ready_unconfirmed', id);
|
|
374
474
|
const prepared = Object.freeze({
|
|
375
475
|
id: preparationId, status: 'ready', psp: input.psp, environment: input.environment, expiresAt: state.ready_expires_at,
|
|
376
|
-
cardId: state.card_id, user: input.user, merchant: input.merchant,
|
|
476
|
+
cardId: state.card_id, user: input.user, merchant: input.merchant, amount: state.amount,
|
|
477
|
+
amountDisplay: typeof state.amount_display === 'string' ? state.amount_display : null,
|
|
377
478
|
currency: input.currency.toLowerCase(), merchantOrigin: input.merchantOrigin, checkoutKey: input.checkoutKey,
|
|
378
|
-
paymentStatus: 'not_started', amountAuthority: '
|
|
479
|
+
paymentStatus: 'not_started', amountAuthority: 'agent',
|
|
379
480
|
});
|
|
380
481
|
this.preparations.add(prepared);
|
|
381
482
|
ready = true;
|
|
@@ -419,7 +520,7 @@ export class VaultClient {
|
|
|
419
520
|
this.usedPreparations.add(preparation);
|
|
420
521
|
if (Date.parse(preparation.expiresAt) <= Date.now())
|
|
421
522
|
throw new CheckoutPreparationError(preparation.id, 'expired');
|
|
422
|
-
if (input.user !== preparation.user || input.merchant !== preparation.merchant || input.
|
|
523
|
+
if (input.user !== preparation.user || input.merchant !== preparation.merchant || (typeof input.amount === 'number' && input.amount !== preparation.amount) || (typeof input.amount === 'string' && !validAmountInput(input.amount))
|
|
423
524
|
|| input.currency?.toLowerCase() !== preparation.currency || input.cardId !== preparation.cardId
|
|
424
525
|
|| !matchesPreparedRequest(preparation.psp, preparation.environment, input.request.url, input.request.method ?? 'POST', input.request.body))
|
|
425
526
|
throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
|
|
@@ -452,16 +553,16 @@ export class VaultClient {
|
|
|
452
553
|
throw new Error(`checkout authorization is POST-only; got ${method} for ${redactUrl(input.request.url)}. ` +
|
|
453
554
|
'A non-POST tokenizer needs recognizer support before it can be intercepted.');
|
|
454
555
|
}
|
|
455
|
-
// The amount
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
const
|
|
556
|
+
// The amount is a hint: a pair or nothing. Refuse half a pair here, before
|
|
557
|
+
// a network call: the API would too, and a retry loop cannot fix a
|
|
558
|
+
// missing field. The processor's own amount is read by Agentcard.
|
|
559
|
+
const hasAmount = input.amount != null;
|
|
459
560
|
const hasCurrency = typeof input.currency === 'string' && input.currency.length > 0;
|
|
460
|
-
if (
|
|
461
|
-
throw new Error('
|
|
561
|
+
if (hasAmount !== hasCurrency) {
|
|
562
|
+
throw new Error('amount and currency go together: pass both or neither.');
|
|
462
563
|
}
|
|
463
|
-
if (!input.amount
|
|
464
|
-
throw new Error('
|
|
564
|
+
if (hasAmount && !validAmountInput(input.amount)) {
|
|
565
|
+
throw new Error('amount is an integer in the currency\'s smallest unit (2306 for $23.06), or a decimal string in normal units ("23.06").');
|
|
465
566
|
}
|
|
466
567
|
const timeoutMs = input.timeoutMs ?? 15 * 60_000;
|
|
467
568
|
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)
|
|
@@ -473,15 +574,18 @@ export class VaultClient {
|
|
|
473
574
|
const payload = {
|
|
474
575
|
user: input.user,
|
|
475
576
|
merchant: input.merchant,
|
|
476
|
-
...(input.amount ? { amount: input.amount } : {}),
|
|
477
577
|
// snake_case on the wire; camelCase is this SDK's convention.
|
|
478
|
-
...(
|
|
578
|
+
...(hasAmount ? { amount: input.amount, currency: input.currency } : {}),
|
|
579
|
+
...(input.pageAmount ? { page_amount: input.pageAmount.amount, page_currency: input.pageAmount.currency } : {}),
|
|
479
580
|
psp: rec.psp,
|
|
480
581
|
// The mode this request will be finished in. The API checks it against
|
|
481
582
|
// the recognizer and refuses a disagreement before a row exists.
|
|
482
583
|
mode,
|
|
483
584
|
...(input.cardId ? { cardId: input.cardId } : {}),
|
|
484
|
-
|
|
585
|
+
// A prepared checkout already carries the page origin as merchant_origin.
|
|
586
|
+
...(preparation
|
|
587
|
+
? { preparation_id: preparation.id, checkout_key: preparation.checkoutKey, merchant_origin: preparation.merchantOrigin }
|
|
588
|
+
: input.pageOrigin ? { checkout_origin: input.pageOrigin } : {}),
|
|
485
589
|
request: {
|
|
486
590
|
url: input.request.url,
|
|
487
591
|
method: input.request.method,
|
|
@@ -621,7 +725,7 @@ export class VaultClient {
|
|
|
621
725
|
authorizationId,
|
|
622
726
|
...response,
|
|
623
727
|
amountVerified: typeof s.amount_verified === 'boolean' ? s.amount_verified : null,
|
|
624
|
-
|
|
728
|
+
chargedAmount: typeof s.charged_amount === 'number' ? s.charged_amount : null,
|
|
625
729
|
chargedCurrency: typeof s.charged_currency === 'string' ? s.charged_currency : null,
|
|
626
730
|
chargedKind: s.charged_kind === 'captured' || s.charged_kind === 'authorized' || s.charged_kind === 'none' ? s.charged_kind : null,
|
|
627
731
|
...amountAuthority,
|
|
@@ -635,6 +739,9 @@ export class VaultClient {
|
|
|
635
739
|
}
|
|
636
740
|
if (s.reason === 'intent_not_confirmable')
|
|
637
741
|
throw new IntentNotConfirmableError(String(created.id));
|
|
742
|
+
if (typeof s.reason === 'string' && PRESET_REFUSAL_REASONS.has(s.reason)) {
|
|
743
|
+
throw new PresetRefusedError(String(created.id), s.reason, typeof s.message === 'string' ? s.message : null, presetOf(s.preset), typeof s.rule === 'string' ? s.rule : null, 'pre_replay', attachmentOf(s.attachment), refusalsOf(s.refusals));
|
|
744
|
+
}
|
|
638
745
|
if (s.reason === 'processor_refused') {
|
|
639
746
|
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);
|
|
640
747
|
}
|
|
@@ -718,6 +825,11 @@ export class VaultClient {
|
|
|
718
825
|
return await this.post('/v2/checkout/authorizations', payload, signal, stopRetries);
|
|
719
826
|
}
|
|
720
827
|
catch (err) {
|
|
828
|
+
if (err instanceof CheckoutApiError && err.code && err.status === 403 && presetOf(err.details.preset)) {
|
|
829
|
+
// A company preset refused this purchase before a row existed.
|
|
830
|
+
const d = err.details;
|
|
831
|
+
throw new PresetRefusedError(null, err.code, typeof d.message === 'string' ? d.message : null, presetOf(d.preset), typeof d.rule === 'string' ? d.rule : null, 'create', attachmentOf(d.attachment), refusalsOf(d.refusals));
|
|
832
|
+
}
|
|
721
833
|
if (err instanceof CheckoutApiError && err.code === 'amount_mismatch') {
|
|
722
834
|
const d = err.details;
|
|
723
835
|
throw new AmountMismatchError(null, Number(d.expected_cents), Number(d.actual_cents), String(d.currency ?? currency ?? ''), d.actual_currency != null ? String(d.actual_currency) : undefined, 'create');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
|
|
1
|
+
export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
|
|
2
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 { CheckoutAttachmentError } from './attachment.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
|
|
1
|
+
export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
|
|
2
2
|
export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
|
|
3
3
|
export { CheckoutAttachmentError } from './attachment.js';
|
|
4
4
|
export { substituteEncryptedFields, SubstitutionError } from './substitute.js';
|
package/dist/preparation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CheckoutPreparationError } from './client.js';
|
|
2
|
+
import { validAmountInput } from './client.js';
|
|
2
3
|
import { matchesPreparedRequest, validPreparationEnvironment, preparationEndpoint } from './prepared-processor.js';
|
|
3
4
|
/** A local, one-use rendezvous. It never starts or retries a merchant request. */
|
|
4
5
|
export class PreparationGate {
|
|
@@ -39,7 +40,7 @@ export class PreparationGate {
|
|
|
39
40
|
const tokenizer = preparationEndpoint(options.psp, options.environment);
|
|
40
41
|
if (!this.opts.vault.isCardRequest(tokenizer, 'POST'))
|
|
41
42
|
throw new CheckoutPreparationError(null, 'processor_interception_unavailable');
|
|
42
|
-
if (!
|
|
43
|
+
if (!validAmountInput(this.opts.amount) || this.opts.amount === 0 || !/^[a-z]{3}$/i.test(this.opts.currency ?? ''))
|
|
43
44
|
throw new CheckoutPreparationError(null, 'amount_required');
|
|
44
45
|
if (signal.aborted)
|
|
45
46
|
throw new CheckoutPreparationError(null, 'cancelled');
|
|
@@ -49,7 +50,7 @@ export class PreparationGate {
|
|
|
49
50
|
throw new CheckoutPreparationError(null, 'merchant_origin_invalid');
|
|
50
51
|
const prepared = await this.opts.vault.prepareCheckout({
|
|
51
52
|
...options, user: this.opts.user, merchant: this.opts.merchant,
|
|
52
|
-
|
|
53
|
+
amount: this.opts.amount, currency: this.opts.currency, cardId: this.opts.cardId,
|
|
53
54
|
merchantOrigin: page.origin, checkoutKey: crypto.randomUUID(), timeoutMs: this.opts.timeoutMs, signal,
|
|
54
55
|
onPreparationCreated: id => this.lifecycle.preparationCreated(id),
|
|
55
56
|
onApprovalUrl: url => {
|
package/package.json
CHANGED