@agent-cards/checkout 0.6.0 → 0.8.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 +12 -0
- package/README.md +31 -24
- package/dist/braintree.d.ts +2 -10
- package/dist/braintree.generated.d.ts +10 -0
- package/dist/braintree.generated.js +302 -0
- package/dist/braintree.js +2 -302
- package/dist/builtin-registry.generated.d.ts +2 -0
- package/dist/builtin-registry.generated.js +1 -0
- package/dist/cdp.d.ts +23 -9
- package/dist/cdp.js +99 -6
- package/dist/client.d.ts +124 -27
- package/dist/client.js +211 -27
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +1 -0
- package/dist/lifecycle.js +27 -1
- package/dist/owned-shop.generated.d.ts +24 -0
- package/dist/owned-shop.generated.js +108 -0
- package/dist/preparation.js +3 -2
- package/dist/registry.d.ts +2 -19
- package/dist/registry.js +3 -188
- package/dist/substitute.d.ts +5 -9
- package/dist/substitute.js +5 -69
- package/dist/substitutions.generated.d.ts +10 -0
- package/dist/substitutions.generated.js +66 -0
- package/package.json +6 -5
package/dist/client.d.ts
CHANGED
|
@@ -19,12 +19,28 @@ 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;
|
|
32
|
+
/** How an existing authorization is being handled; omitted by older APIs. */
|
|
33
|
+
export interface ExecutionMetadata {
|
|
34
|
+
executionMode?: 'user_approval' | 'autopilot';
|
|
35
|
+
grantId?: string;
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* The token flow: the cardholder's device called the processor itself, and
|
|
25
39
|
* this is the processor's answer to replay into the paused request.
|
|
26
40
|
*/
|
|
27
|
-
export interface TokenReplay {
|
|
41
|
+
export interface TokenReplay extends ExecutionMetadata {
|
|
42
|
+
/** Present only after validating the explicit owned-shop purchase receipt. */
|
|
43
|
+
shopOrderId?: string;
|
|
28
44
|
/** Absent on older API versions; always 'token' here. */
|
|
29
45
|
mode?: 'token';
|
|
30
46
|
/** The approved authorization (`cauth_…`). */
|
|
@@ -41,17 +57,18 @@ export interface TokenReplay {
|
|
|
41
57
|
* moved) and Agentcard also sent your server checkout_authorization.amount_mismatch.
|
|
42
58
|
*/
|
|
43
59
|
amountVerified?: boolean | null;
|
|
44
|
-
|
|
60
|
+
/** What the processor collected, an integer in the currency's smallest unit (Stripe's `amount`). */
|
|
61
|
+
chargedAmount?: number | null;
|
|
45
62
|
chargedCurrency?: string | null;
|
|
46
63
|
/**
|
|
47
|
-
* What `
|
|
64
|
+
* What `chargedAmount` is: 'captured' (the intent succeeded; this is
|
|
48
65
|
* amount_received), 'authorized' (requires_capture; amount_capturable, the
|
|
49
66
|
* merchant captures later), 'none' (a PaymentIntent was reported but
|
|
50
67
|
* nothing is collected yet: processing, requires_action), or null when no
|
|
51
68
|
* PaymentIntent was reported at all (a tokenization request).
|
|
52
69
|
*/
|
|
53
70
|
chargedKind?: 'captured' | 'authorized' | 'none' | null;
|
|
54
|
-
/**
|
|
71
|
+
/** Who named the amount the person approved. */
|
|
55
72
|
amountAuthority?: AmountAuthority;
|
|
56
73
|
}
|
|
57
74
|
/**
|
|
@@ -63,7 +80,7 @@ export interface TokenReplay {
|
|
|
63
80
|
* The processor's answer then reaches the page as it normally would; the
|
|
64
81
|
* merchant's order state is where the outcome shows up.
|
|
65
82
|
*/
|
|
66
|
-
export interface CseReplay {
|
|
83
|
+
export interface CseReplay extends ExecutionMetadata {
|
|
67
84
|
mode: 'cse';
|
|
68
85
|
authorizationId: string;
|
|
69
86
|
substitutions: Substitutions;
|
|
@@ -86,7 +103,7 @@ export interface CseReplay {
|
|
|
86
103
|
* processor evidence on this mode and cannot obtain any. Treat it as "the
|
|
87
104
|
* person paid, or tried to, on their own device" and confirm with the merchant.
|
|
88
105
|
*/
|
|
89
|
-
export interface HostedFormReplay {
|
|
106
|
+
export interface HostedFormReplay extends ExecutionMetadata {
|
|
90
107
|
mode: 'hosted_form';
|
|
91
108
|
/** What this resolution is: a device-attested submission, not a processor answer. */
|
|
92
109
|
kind: 'submitted_on_device';
|
|
@@ -108,7 +125,8 @@ export interface PrepareCheckoutOptions {
|
|
|
108
125
|
export interface PrepareCheckoutInput extends PrepareCheckoutOptions {
|
|
109
126
|
user: string;
|
|
110
127
|
merchant: string;
|
|
111
|
-
|
|
128
|
+
/** An integer in the currency's smallest unit (2306 for $23.06), or a decimal string in normal units ("23.06"). */
|
|
129
|
+
amount: number | string;
|
|
112
130
|
currency: string;
|
|
113
131
|
cardId?: string;
|
|
114
132
|
merchantOrigin: string;
|
|
@@ -127,42 +145,50 @@ export interface PreparedCheckout {
|
|
|
127
145
|
readonly cardId: string;
|
|
128
146
|
readonly user: string;
|
|
129
147
|
readonly merchant: string;
|
|
130
|
-
|
|
148
|
+
/** The approved amount, an integer in the currency's smallest unit, as the API confirmed it. */
|
|
149
|
+
readonly amount: number;
|
|
150
|
+
readonly amountDisplay: string | null;
|
|
131
151
|
readonly currency: string;
|
|
132
152
|
readonly merchantOrigin: string;
|
|
133
153
|
readonly checkoutKey: string;
|
|
134
154
|
readonly paymentStatus: 'not_started';
|
|
135
|
-
readonly amountAuthority: '
|
|
155
|
+
readonly amountAuthority: 'agent';
|
|
136
156
|
}
|
|
137
157
|
export declare class CheckoutPreparationError extends Error {
|
|
138
158
|
preparationId: string | null;
|
|
139
159
|
reason: string;
|
|
140
160
|
constructor(preparationId: string | null, reason: string);
|
|
141
161
|
}
|
|
142
|
-
export interface AuthorizeInput {
|
|
162
|
+
export interface AuthorizeInput extends ExecutionMetadata {
|
|
163
|
+
/** Observed top-level HTTPS merchant origin; a routing hint, never payment authority. */
|
|
164
|
+
merchantOrigin?: string;
|
|
143
165
|
/** Your identifier for the person whose card should pay. */
|
|
144
166
|
user: string;
|
|
145
|
-
/** Shown to the user on the approval screen. */
|
|
167
|
+
/** Shown to the user on the approval screen. Judged by nothing: the merchant comes from the checkout page. */
|
|
146
168
|
merchant: string;
|
|
147
169
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
170
|
+
* Your hint at the amount: an integer in the currency's smallest unit (2306
|
|
171
|
+
* for $23.06), or a decimal string in normal units ("23.06"), with its ISO
|
|
172
|
+
* 4217 code ("usd"). Optional: the processor's own amount is the higher
|
|
173
|
+
* authority, read from the paused request or from the Stripe intent it
|
|
174
|
+
* names right before the cardholder's device replays, and the company's
|
|
175
|
+
* caps are judged on it. A hint lets a bad purchase be refused the moment
|
|
176
|
+
* you open it, and a hint that disagrees with the processor beyond one
|
|
177
|
+
* smallest unit is refused with nothing charged (an AmountMismatchError;
|
|
178
|
+
* `stage` says which check). Agentcard derives the display string; you
|
|
179
|
+
* never send one. Both or neither: one without the other is refused.
|
|
151
180
|
*/
|
|
152
|
-
amount?: string;
|
|
181
|
+
amount?: number | string;
|
|
182
|
+
currency?: string;
|
|
153
183
|
/**
|
|
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.
|
|
184
|
+
* The total read off the checkout page, the lowest authority: used only
|
|
185
|
+
* when neither the processor's request nor your hint names an amount. The
|
|
186
|
+
* adapters fill it from `[data-agentcard-amount]` when a page carries one.
|
|
163
187
|
*/
|
|
164
|
-
|
|
165
|
-
|
|
188
|
+
pageAmount?: {
|
|
189
|
+
amount: number;
|
|
190
|
+
currency: string;
|
|
191
|
+
};
|
|
166
192
|
/**
|
|
167
193
|
* WHICH stored card should pay — a vault card id from
|
|
168
194
|
* GET /api/v2/vault_cards. The approval page preselects it (the human can
|
|
@@ -171,6 +197,14 @@ export interface AuthorizeInput {
|
|
|
171
197
|
* `card_not_found`.
|
|
172
198
|
*/
|
|
173
199
|
cardId?: string;
|
|
200
|
+
/**
|
|
201
|
+
* The origin of the checkout page the payment form was on
|
|
202
|
+
* (https://shop.example.com). The adapters read it from the page; pass it
|
|
203
|
+
* yourself when you run your own interception. With the processor identity
|
|
204
|
+
* in the paused request this is what names the merchant for the company's
|
|
205
|
+
* presets; `merchant` above is shown to the person and judged by nothing.
|
|
206
|
+
*/
|
|
207
|
+
pageOrigin?: string;
|
|
174
208
|
request: PausedRequest;
|
|
175
209
|
/** Abort if the user has not approved within this many ms. Default 15 min. */
|
|
176
210
|
timeoutMs?: number;
|
|
@@ -300,6 +334,69 @@ export declare class ProcessorRefusedError extends ApprovalDeclinedError {
|
|
|
300
334
|
* retrying is worth anything: a 404 `connection_not_found` will answer the same
|
|
301
335
|
* way forever, while a 429 or a 502 will not.
|
|
302
336
|
*/
|
|
337
|
+
/**
|
|
338
|
+
* The company's preset refused the purchase. The company that runs this
|
|
339
|
+
* integration put rules on its users' Vault purchases (a merchant list, a
|
|
340
|
+
* currency, a spend cap, a time window); this purchase is outside them.
|
|
341
|
+
* Nothing was charged. Two stages:
|
|
342
|
+
* - 'create': refused before any authorization existed (`authorizationId`
|
|
343
|
+
* is null); nobody was asked to approve. Per purchase, not per page: the
|
|
344
|
+
* next request on the same page is judged afresh.
|
|
345
|
+
* - 'pre_replay': refused right before the cardholder's device would have
|
|
346
|
+
* sent the card; the authorization is `declined` with `code` as reason.
|
|
347
|
+
* `code` is the rule's reason (merchant_denied, currency_denied,
|
|
348
|
+
* spend_rate_exceeded, time_window_denied, ...), `message` the rule's own
|
|
349
|
+
* statement with the company's next step, `preset` the version that judged
|
|
350
|
+
* it. When several presets refused the same purchase, `code`, `preset`,
|
|
351
|
+
* `rule` and `attachment` are the first, and `refusals` names every one. A
|
|
352
|
+
* decline in every structural sense (the adapters quiet the page's retry as
|
|
353
|
+
* for a person's "no"), so it extends ApprovalDeclinedError.
|
|
354
|
+
*/
|
|
355
|
+
export declare class PresetRefusedError extends ApprovalDeclinedError {
|
|
356
|
+
readonly authorizationId: string | null;
|
|
357
|
+
readonly code: string;
|
|
358
|
+
readonly detail: string | null;
|
|
359
|
+
readonly preset: {
|
|
360
|
+
id: string;
|
|
361
|
+
version: number;
|
|
362
|
+
name: string;
|
|
363
|
+
} | null;
|
|
364
|
+
readonly rule: string | null;
|
|
365
|
+
readonly stage: 'create' | 'pre_replay';
|
|
366
|
+
/** The stored card the preset that refused is attached to, with its last four digits. */
|
|
367
|
+
readonly attachment: PresetAttachment | null;
|
|
368
|
+
/** The preset's name. */
|
|
369
|
+
readonly presetName: string | null;
|
|
370
|
+
/** Every preset that refused, each with its attachment, rule, code and statement; one entry when one refused. */
|
|
371
|
+
readonly refusals: readonly PresetRefusal[];
|
|
372
|
+
constructor(authorizationId: string | null, code: string, detail: string | null, preset: {
|
|
373
|
+
id: string;
|
|
374
|
+
version: number;
|
|
375
|
+
name: string;
|
|
376
|
+
} | null, rule: string | null, stage: 'create' | 'pre_replay',
|
|
377
|
+
/** The stored card the preset that refused is attached to, with its last four digits. */
|
|
378
|
+
attachment?: PresetAttachment | null, refusals?: readonly PresetRefusal[]);
|
|
379
|
+
}
|
|
380
|
+
/** The stored card a preset is attached to, with its last four digits. */
|
|
381
|
+
export interface PresetAttachment {
|
|
382
|
+
kind: 'card';
|
|
383
|
+
targetId: string | null;
|
|
384
|
+
last4: string | null;
|
|
385
|
+
}
|
|
386
|
+
/** One preset's refusal of a purchase. */
|
|
387
|
+
export interface PresetRefusal {
|
|
388
|
+
preset: {
|
|
389
|
+
id: string;
|
|
390
|
+
version: number;
|
|
391
|
+
name: string;
|
|
392
|
+
};
|
|
393
|
+
attachment: PresetAttachment | null;
|
|
394
|
+
rule: string | null;
|
|
395
|
+
code: string;
|
|
396
|
+
detail: string | null;
|
|
397
|
+
}
|
|
398
|
+
/** A decline reason only the company presets stamp (see PresetRefusedError). */
|
|
399
|
+
export declare const PRESET_REFUSAL_REASONS: ReadonlySet<string>;
|
|
303
400
|
export declare class CheckoutApiError extends Error {
|
|
304
401
|
status: number;
|
|
305
402
|
path: string;
|
package/dist/client.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { BUILTIN_REGISTRY, cardUrlPatterns as deriveCardUrlPatterns, findRecognizer, } from './registry.js';
|
|
2
2
|
import { matchesPreparedRequest, validPreparationEnvironment } from './prepared-processor.js';
|
|
3
|
+
import { hasOwnedShopMarker, parseOwnedShopOrder, parseOwnedShopReceipt } from './owned-shop.generated.js';
|
|
3
4
|
/**
|
|
4
5
|
* The modes this SDK can finish. Asked for on syncRegistry (the API serves
|
|
5
6
|
* only recognizers in these modes, so a request this build cannot complete
|
|
6
7
|
* is never paused) and sent on every create.
|
|
7
8
|
*/
|
|
8
9
|
export const SUPPORTED_MODES = ['token', 'cse', 'hosted_form'];
|
|
9
|
-
|
|
10
|
+
/** An integer in the smallest unit, or a decimal string in normal units with a point; nothing else. */
|
|
11
|
+
export function validAmountInput(amount) {
|
|
12
|
+
if (typeof amount === 'number')
|
|
13
|
+
return Number.isSafeInteger(amount) && amount >= 0;
|
|
14
|
+
return typeof amount === 'string' && /^\d{1,15}\.\d{1,3}$/.test(amount.trim());
|
|
15
|
+
}
|
|
16
|
+
const AMOUNT_AUTHORITIES = ['processor', 'agent', 'page', 'none'];
|
|
10
17
|
export class CheckoutPreparationError extends Error {
|
|
11
18
|
preparationId;
|
|
12
19
|
reason;
|
|
@@ -180,6 +187,94 @@ export class ProcessorRefusedError extends ApprovalDeclinedError {
|
|
|
180
187
|
* retrying is worth anything: a 404 `connection_not_found` will answer the same
|
|
181
188
|
* way forever, while a 429 or a 502 will not.
|
|
182
189
|
*/
|
|
190
|
+
/**
|
|
191
|
+
* The company's preset refused the purchase. The company that runs this
|
|
192
|
+
* integration put rules on its users' Vault purchases (a merchant list, a
|
|
193
|
+
* currency, a spend cap, a time window); this purchase is outside them.
|
|
194
|
+
* Nothing was charged. Two stages:
|
|
195
|
+
* - 'create': refused before any authorization existed (`authorizationId`
|
|
196
|
+
* is null); nobody was asked to approve. Per purchase, not per page: the
|
|
197
|
+
* next request on the same page is judged afresh.
|
|
198
|
+
* - 'pre_replay': refused right before the cardholder's device would have
|
|
199
|
+
* sent the card; the authorization is `declined` with `code` as reason.
|
|
200
|
+
* `code` is the rule's reason (merchant_denied, currency_denied,
|
|
201
|
+
* spend_rate_exceeded, time_window_denied, ...), `message` the rule's own
|
|
202
|
+
* statement with the company's next step, `preset` the version that judged
|
|
203
|
+
* it. When several presets refused the same purchase, `code`, `preset`,
|
|
204
|
+
* `rule` and `attachment` are the first, and `refusals` names every one. A
|
|
205
|
+
* decline in every structural sense (the adapters quiet the page's retry as
|
|
206
|
+
* for a person's "no"), so it extends ApprovalDeclinedError.
|
|
207
|
+
*/
|
|
208
|
+
export class PresetRefusedError extends ApprovalDeclinedError {
|
|
209
|
+
authorizationId;
|
|
210
|
+
code;
|
|
211
|
+
detail;
|
|
212
|
+
preset;
|
|
213
|
+
rule;
|
|
214
|
+
stage;
|
|
215
|
+
attachment;
|
|
216
|
+
/** The preset's name. */
|
|
217
|
+
presetName;
|
|
218
|
+
/** Every preset that refused, each with its attachment, rule, code and statement; one entry when one refused. */
|
|
219
|
+
refusals;
|
|
220
|
+
constructor(authorizationId, code, detail, preset, rule, stage,
|
|
221
|
+
/** The stored card the preset that refused is attached to, with its last four digits. */
|
|
222
|
+
attachment = null, refusals = []) {
|
|
223
|
+
super(code);
|
|
224
|
+
this.authorizationId = authorizationId;
|
|
225
|
+
this.code = code;
|
|
226
|
+
this.detail = detail;
|
|
227
|
+
this.preset = preset;
|
|
228
|
+
this.rule = rule;
|
|
229
|
+
this.stage = stage;
|
|
230
|
+
this.attachment = attachment;
|
|
231
|
+
this.name = 'PresetRefusedError';
|
|
232
|
+
this.presetName = preset?.name ?? null;
|
|
233
|
+
this.refusals = refusals.length ? refusals : preset ? [{ preset, attachment, rule, code, detail }] : [];
|
|
234
|
+
const others = this.refusals.slice(1).map((r) => `"${r.preset.name}"`);
|
|
235
|
+
const also = others.length ? ` Also refused by ${others.length === 1 ? 'preset' : 'presets'} ${others.join(', ')}.` : '';
|
|
236
|
+
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}`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** A decline reason only the company presets stamp (see PresetRefusedError). */
|
|
240
|
+
export const PRESET_REFUSAL_REASONS = new Set([
|
|
241
|
+
'spend_total_exceeded', 'spend_rate_exceeded', 'spend_rate_unknown', 'amount_unknown', 'rate_unavailable',
|
|
242
|
+
'category_denied', 'category_unknown', 'merchant_denied', 'merchant_unknown',
|
|
243
|
+
'geo_denied', 'geo_unknown', 'currency_denied', 'currency_unknown',
|
|
244
|
+
'time_window_denied', 'surface_denied', 'surface_unknown',
|
|
245
|
+
]);
|
|
246
|
+
function presetOf(v) {
|
|
247
|
+
if (!v || typeof v !== 'object')
|
|
248
|
+
return null;
|
|
249
|
+
const p = v;
|
|
250
|
+
if (typeof p.id !== 'string' || typeof p.version !== 'number' || typeof p.name !== 'string')
|
|
251
|
+
return null;
|
|
252
|
+
return { id: p.id, version: p.version, name: p.name };
|
|
253
|
+
}
|
|
254
|
+
function attachmentOf(v) {
|
|
255
|
+
if (!v || typeof v !== 'object')
|
|
256
|
+
return null;
|
|
257
|
+
const a = v;
|
|
258
|
+
if (a.kind !== 'card')
|
|
259
|
+
return null;
|
|
260
|
+
return { kind: a.kind, targetId: typeof a.target_id === 'string' ? a.target_id : null, last4: typeof a.last4 === 'string' ? a.last4 : null };
|
|
261
|
+
}
|
|
262
|
+
/** The `refusals` list of a refusal envelope: every entry that names a preset. */
|
|
263
|
+
function refusalsOf(v) {
|
|
264
|
+
if (!Array.isArray(v))
|
|
265
|
+
return [];
|
|
266
|
+
const out = [];
|
|
267
|
+
for (const item of v) {
|
|
268
|
+
if (!item || typeof item !== 'object')
|
|
269
|
+
continue;
|
|
270
|
+
const r = item;
|
|
271
|
+
const preset = presetOf(r.preset);
|
|
272
|
+
if (!preset || typeof r.reason !== 'string')
|
|
273
|
+
continue;
|
|
274
|
+
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 });
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
183
278
|
export class CheckoutApiError extends Error {
|
|
184
279
|
status;
|
|
185
280
|
path;
|
|
@@ -218,6 +313,12 @@ export class CheckoutApiError extends Error {
|
|
|
218
313
|
get permanent() {
|
|
219
314
|
if (this.code === 'amount_mismatch' || this.code === 'duplicate_submission')
|
|
220
315
|
return false;
|
|
316
|
+
// Nor a refusal by the company preset (a 403 carrying `preset` in the
|
|
317
|
+
// envelope): the company's rules refused THIS purchase, at this merchant,
|
|
318
|
+
// for this amount, at this hour. The next one on the same page may pass,
|
|
319
|
+
// so the page is quieted like a decline, never latched.
|
|
320
|
+
if (this.details.preset && typeof this.details.preset === 'object')
|
|
321
|
+
return false;
|
|
221
322
|
return this.status >= 400 && this.status < 500 && this.status !== 429;
|
|
222
323
|
}
|
|
223
324
|
}
|
|
@@ -317,7 +418,7 @@ export class VaultClient {
|
|
|
317
418
|
const fail = (reason, id = null) => new CheckoutPreparationError(id, reason);
|
|
318
419
|
if (!validPreparationEnvironment(input.psp, input.environment))
|
|
319
420
|
throw fail('unsupported_processor');
|
|
320
|
-
if (!
|
|
421
|
+
if (!validAmountInput(input.amount) || typeof input.currency !== 'string' || !/^[a-z]{3}$/i.test(input.currency))
|
|
321
422
|
throw fail('amount_required');
|
|
322
423
|
const origin = new URL(input.merchantOrigin);
|
|
323
424
|
if (!(origin.protocol === 'https:' || (origin.protocol === 'http:' && origin.hostname === 'localhost')) || origin.origin !== input.merchantOrigin)
|
|
@@ -336,7 +437,7 @@ export class VaultClient {
|
|
|
336
437
|
// Drain a sent creation even after caller cancellation to retire its ID.
|
|
337
438
|
// The separate stop signal prevents dispatch after a slow OAuth exchange.
|
|
338
439
|
const created = await this.post('/v2/checkout/preparations', {
|
|
339
|
-
user: input.user, merchant: input.merchant,
|
|
440
|
+
user: input.user, merchant: input.merchant, amount: input.amount, currency: input.currency.toLowerCase(),
|
|
340
441
|
...(input.cardId ? { card_id: input.cardId } : {}), psp: input.psp, mode: 'token',
|
|
341
442
|
environment: input.environment, checkout_key: input.checkoutKey, merchant_origin: input.merchantOrigin,
|
|
342
443
|
}, AbortSignal.timeout(30_000), signal);
|
|
@@ -365,17 +466,18 @@ export class VaultClient {
|
|
|
365
466
|
if (state.status === 'ready') {
|
|
366
467
|
const expiry = Date.parse(state.ready_expires_at);
|
|
367
468
|
if (!Number.isFinite(expiry) || expiry <= Date.now() || typeof state.card_id !== 'string' || !state.card_id
|
|
368
|
-
|| state.payment_status !== 'not_started' || state.amount_authority !== '
|
|
469
|
+
|| state.payment_status !== 'not_started' || state.amount_authority !== 'agent'
|
|
369
470
|
|| state.user !== input.user || state.merchant !== input.merchant || state.merchant_origin !== input.merchantOrigin
|
|
370
|
-
|| state.
|
|
471
|
+
|| !Number.isSafeInteger(state.amount) || (typeof input.amount === 'number' && state.amount !== input.amount) || state.currency !== input.currency.toLowerCase()
|
|
371
472
|
|| state.psp !== input.psp || state.mode !== 'token' || state.environment !== input.environment
|
|
372
473
|
|| state.checkout_key !== input.checkoutKey)
|
|
373
474
|
throw fail('ready_unconfirmed', id);
|
|
374
475
|
const prepared = Object.freeze({
|
|
375
476
|
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,
|
|
477
|
+
cardId: state.card_id, user: input.user, merchant: input.merchant, amount: state.amount,
|
|
478
|
+
amountDisplay: typeof state.amount_display === 'string' ? state.amount_display : null,
|
|
377
479
|
currency: input.currency.toLowerCase(), merchantOrigin: input.merchantOrigin, checkoutKey: input.checkoutKey,
|
|
378
|
-
paymentStatus: 'not_started', amountAuthority: '
|
|
480
|
+
paymentStatus: 'not_started', amountAuthority: 'agent',
|
|
379
481
|
});
|
|
380
482
|
this.preparations.add(prepared);
|
|
381
483
|
ready = true;
|
|
@@ -411,6 +513,27 @@ export class VaultClient {
|
|
|
411
513
|
* response to replay into the browser. Your process never sees a card.
|
|
412
514
|
*/
|
|
413
515
|
async authorize(input) {
|
|
516
|
+
const ownedShop = hasOwnedShopMarker(input.request);
|
|
517
|
+
const shopOrder = ownedShop ? parseOwnedShopOrder(input.request) : null;
|
|
518
|
+
if (ownedShop) {
|
|
519
|
+
if (!shopOrder || input.preparation || input.executionMode === 'user_approval' ||
|
|
520
|
+
!input.merchantOrigin || (input.amount != null && input.amount !== shopOrder.amount &&
|
|
521
|
+
!(typeof input.amount === 'string' && Number(input.amount) * 100 === shopOrder.amount)) ||
|
|
522
|
+
(input.currency !== undefined && input.currency.toLowerCase() !== shopOrder.currency))
|
|
523
|
+
throw new Error('The shop order cannot use this checkout configuration.');
|
|
524
|
+
input = { ...input, amount: shopOrder.amount, currency: shopOrder.currency };
|
|
525
|
+
}
|
|
526
|
+
if (input.executionMode !== undefined && !['user_approval', 'autopilot'].includes(input.executionMode))
|
|
527
|
+
throw new Error('Unsupported checkout execution mode.');
|
|
528
|
+
if (input.grantId !== undefined && !/^apg_[A-Za-z0-9_-]{1,128}$/.test(input.grantId))
|
|
529
|
+
throw new Error('Invalid autopilot grant ID.');
|
|
530
|
+
if (input.preparation && (input.executionMode === 'autopilot' || input.grantId))
|
|
531
|
+
throw new Error('A device preparation cannot also select autopilot.');
|
|
532
|
+
if (input.merchantOrigin !== undefined && !input.preparation) {
|
|
533
|
+
const merchantUrl = new URL(input.merchantOrigin);
|
|
534
|
+
if (merchantUrl.protocol !== 'https:' || merchantUrl.origin !== input.merchantOrigin)
|
|
535
|
+
throw new Error('merchantOrigin must be an exact HTTPS origin.');
|
|
536
|
+
}
|
|
414
537
|
const preparation = input.preparation;
|
|
415
538
|
if (preparation) {
|
|
416
539
|
if (!this.preparations.has(preparation) || this.usedPreparations.has(preparation))
|
|
@@ -419,7 +542,7 @@ export class VaultClient {
|
|
|
419
542
|
this.usedPreparations.add(preparation);
|
|
420
543
|
if (Date.parse(preparation.expiresAt) <= Date.now())
|
|
421
544
|
throw new CheckoutPreparationError(preparation.id, 'expired');
|
|
422
|
-
if (input.user !== preparation.user || input.merchant !== preparation.merchant || input.
|
|
545
|
+
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
546
|
|| input.currency?.toLowerCase() !== preparation.currency || input.cardId !== preparation.cardId
|
|
424
547
|
|| !matchesPreparedRequest(preparation.psp, preparation.environment, input.request.url, input.request.method ?? 'POST', input.request.body))
|
|
425
548
|
throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
|
|
@@ -452,16 +575,16 @@ export class VaultClient {
|
|
|
452
575
|
throw new Error(`checkout authorization is POST-only; got ${method} for ${redactUrl(input.request.url)}. ` +
|
|
453
576
|
'A non-POST tokenizer needs recognizer support before it can be intercepted.');
|
|
454
577
|
}
|
|
455
|
-
// The amount
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
const
|
|
578
|
+
// The amount is a hint: a pair or nothing. Refuse half a pair here, before
|
|
579
|
+
// a network call: the API would too, and a retry loop cannot fix a
|
|
580
|
+
// missing field. The processor's own amount is read by Agentcard.
|
|
581
|
+
const hasAmount = input.amount != null;
|
|
459
582
|
const hasCurrency = typeof input.currency === 'string' && input.currency.length > 0;
|
|
460
|
-
if (
|
|
461
|
-
throw new Error('
|
|
583
|
+
if (hasAmount !== hasCurrency) {
|
|
584
|
+
throw new Error('amount and currency go together: pass both or neither.');
|
|
462
585
|
}
|
|
463
|
-
if (!input.amount
|
|
464
|
-
throw new Error('
|
|
586
|
+
if (hasAmount && !validAmountInput(input.amount)) {
|
|
587
|
+
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
588
|
}
|
|
466
589
|
const timeoutMs = input.timeoutMs ?? 15 * 60_000;
|
|
467
590
|
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647)
|
|
@@ -473,15 +596,21 @@ export class VaultClient {
|
|
|
473
596
|
const payload = {
|
|
474
597
|
user: input.user,
|
|
475
598
|
merchant: input.merchant,
|
|
476
|
-
...(input.amount ? { amount: input.amount } : {}),
|
|
477
599
|
// snake_case on the wire; camelCase is this SDK's convention.
|
|
478
|
-
...(
|
|
600
|
+
...(hasAmount ? { amount: input.amount, currency: input.currency } : {}),
|
|
601
|
+
...(input.pageAmount ? { page_amount: input.pageAmount.amount, page_currency: input.pageAmount.currency } : {}),
|
|
479
602
|
psp: rec.psp,
|
|
480
603
|
// The mode this request will be finished in. The API checks it against
|
|
481
604
|
// the recognizer and refuses a disagreement before a row exists.
|
|
482
605
|
mode,
|
|
483
606
|
...(input.cardId ? { cardId: input.cardId } : {}),
|
|
484
|
-
...(
|
|
607
|
+
...(input.executionMode ? { execution_mode: input.executionMode } : {}),
|
|
608
|
+
...(input.grantId ? { grant_id: input.grantId } : {}),
|
|
609
|
+
...(input.merchantOrigin && !preparation ? { merchant_origin: input.merchantOrigin } : {}),
|
|
610
|
+
// A prepared checkout already carries the page origin as merchant_origin.
|
|
611
|
+
...(preparation
|
|
612
|
+
? { preparation_id: preparation.id, checkout_key: preparation.checkoutKey, merchant_origin: preparation.merchantOrigin }
|
|
613
|
+
: input.pageOrigin ? { checkout_origin: input.pageOrigin } : {}),
|
|
485
614
|
request: {
|
|
486
615
|
url: input.request.url,
|
|
487
616
|
method: input.request.method,
|
|
@@ -521,6 +650,20 @@ export class VaultClient {
|
|
|
521
650
|
if (!created || typeof created.id !== 'string' || !created.id)
|
|
522
651
|
throw new PaymentOutcomeUnknownError(preparation ? await this.retireUncertainPreparation(preparation, input.onAuthorizationCreated) : null, 'authorization_create_malformed');
|
|
523
652
|
const authorizationId = created.id;
|
|
653
|
+
let execution = {};
|
|
654
|
+
let approvalDelivered = false;
|
|
655
|
+
const deliverApproval = (state) => {
|
|
656
|
+
if (ownedShop || preparation || approvalDelivered || execution.executionMode === 'autopilot')
|
|
657
|
+
return;
|
|
658
|
+
const url = state.approvalUrl ?? state.approval_url;
|
|
659
|
+
if (typeof url !== 'string' || !url)
|
|
660
|
+
return;
|
|
661
|
+
approvalDelivered = true;
|
|
662
|
+
try {
|
|
663
|
+
Promise.resolve(input.onApprovalUrl?.(url)).catch(() => { });
|
|
664
|
+
}
|
|
665
|
+
catch { /* observer only */ }
|
|
666
|
+
};
|
|
524
667
|
let failed = false;
|
|
525
668
|
const stopSignal = input.merchantSignal
|
|
526
669
|
? AbortSignal.any([input.merchantSignal, ...(input.signal ? [input.signal] : [])]) : input.signal;
|
|
@@ -531,12 +674,8 @@ export class VaultClient {
|
|
|
531
674
|
catch { /* observer only */ }
|
|
532
675
|
if (input.merchantSignal?.aborted)
|
|
533
676
|
throw new PaymentOutcomeUnknownError(authorizationId, 'merchant_request_aborted');
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
Promise.resolve(input.onApprovalUrl?.(created.approvalUrl)).catch(() => { });
|
|
537
|
-
}
|
|
538
|
-
catch { /* approval delivery must not lose an existing authorization */ }
|
|
539
|
-
}
|
|
677
|
+
execution = executionMetadata(created, authorizationId);
|
|
678
|
+
deliverApproval(created);
|
|
540
679
|
while (Date.now() < deadline) {
|
|
541
680
|
if (stopSignal?.aborted)
|
|
542
681
|
throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
|
|
@@ -557,6 +696,11 @@ export class VaultClient {
|
|
|
557
696
|
if (!s || typeof s !== 'object' || Array.isArray(s) || typeof s.status !== 'string') {
|
|
558
697
|
throw new PaymentOutcomeUnknownError(authorizationId, 'authorization_status_malformed');
|
|
559
698
|
}
|
|
699
|
+
execution = executionMetadata(s, authorizationId, execution);
|
|
700
|
+
if (shopOrder && (s.status === 'submitted_on_device' ||
|
|
701
|
+
(s.status === 'approved' && (s.mode !== 'token' || execution.executionMode !== 'autopilot'))))
|
|
702
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'shop_receipt_unconfirmed');
|
|
703
|
+
deliverApproval(s);
|
|
560
704
|
const amountAuthority = typeof s.amount_authority === 'string' && AMOUNT_AUTHORITIES.includes(s.amount_authority)
|
|
561
705
|
? { amountAuthority: s.amount_authority }
|
|
562
706
|
: {};
|
|
@@ -572,7 +716,9 @@ export class VaultClient {
|
|
|
572
716
|
if (typeof s.submitted_at !== 'string' || !s.submitted_at) {
|
|
573
717
|
throw new PaymentOutcomeUnknownError(authorizationId, 'submission_timestamp_missing');
|
|
574
718
|
}
|
|
575
|
-
|
|
719
|
+
if (execution.executionMode === 'autopilot')
|
|
720
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
|
|
721
|
+
return { mode: 'hosted_form', kind: 'submitted_on_device', outcome: 'unverified', authorizationId, submittedAt: s.submitted_at, ...amountAuthority, ...execution };
|
|
576
722
|
}
|
|
577
723
|
if (s.status === 'approved') {
|
|
578
724
|
const approvedMode = typeof s.mode === 'string' ? s.mode : 'token';
|
|
@@ -583,6 +729,8 @@ export class VaultClient {
|
|
|
583
729
|
throw new PaymentOutcomeUnknownError(authorizationId, 'hosted_form_approval_malformed');
|
|
584
730
|
}
|
|
585
731
|
if (approvedMode === 'cse') {
|
|
732
|
+
if (execution.executionMode === 'autopilot')
|
|
733
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
|
|
586
734
|
const sub = s.substitutions;
|
|
587
735
|
const fieldsOk = sub && typeof sub === 'object' && sub.encoding === 'json' && typeof sub.at === 'string' && sub.at
|
|
588
736
|
&& sub.fields && typeof sub.fields === 'object' && !Array.isArray(sub.fields)
|
|
@@ -607,6 +755,7 @@ export class VaultClient {
|
|
|
607
755
|
...(removeRaw ? { remove: [...removeRaw] } : {}),
|
|
608
756
|
},
|
|
609
757
|
...amountAuthority,
|
|
758
|
+
...execution,
|
|
610
759
|
};
|
|
611
760
|
}
|
|
612
761
|
if (approvedMode !== 'token')
|
|
@@ -616,15 +765,29 @@ export class VaultClient {
|
|
|
616
765
|
|| typeof response.body !== 'string' || !response.headers || typeof response.headers !== 'object' || Array.isArray(response.headers)) {
|
|
617
766
|
throw new PaymentOutcomeUnknownError(authorizationId, 'approved_response_malformed');
|
|
618
767
|
}
|
|
768
|
+
if (shopOrder) {
|
|
769
|
+
let receipt;
|
|
770
|
+
try {
|
|
771
|
+
receipt = parseOwnedShopReceipt(JSON.parse(response.body), shopOrder);
|
|
772
|
+
}
|
|
773
|
+
catch { /* No raw error or token replay. */ }
|
|
774
|
+
if (execution.executionMode !== 'autopilot' || response.status !== 200 || !receipt)
|
|
775
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'shop_receipt_unconfirmed');
|
|
776
|
+
// Only canonical receipt fields and our content type enter the page.
|
|
777
|
+
response.body = JSON.stringify(receipt);
|
|
778
|
+
response.headers = { 'content-type': 'application/json' };
|
|
779
|
+
}
|
|
619
780
|
return {
|
|
620
781
|
mode: 'token',
|
|
621
782
|
authorizationId,
|
|
783
|
+
...(shopOrder ? { shopOrderId: shopOrder.order_id } : {}),
|
|
622
784
|
...response,
|
|
623
785
|
amountVerified: typeof s.amount_verified === 'boolean' ? s.amount_verified : null,
|
|
624
|
-
|
|
786
|
+
chargedAmount: typeof s.charged_amount === 'number' ? s.charged_amount : null,
|
|
625
787
|
chargedCurrency: typeof s.charged_currency === 'string' ? s.charged_currency : null,
|
|
626
788
|
chargedKind: s.charged_kind === 'captured' || s.charged_kind === 'authorized' || s.charged_kind === 'none' ? s.charged_kind : null,
|
|
627
789
|
...amountAuthority,
|
|
790
|
+
...execution,
|
|
628
791
|
};
|
|
629
792
|
}
|
|
630
793
|
if (s.status === 'declined') {
|
|
@@ -635,6 +798,9 @@ export class VaultClient {
|
|
|
635
798
|
}
|
|
636
799
|
if (s.reason === 'intent_not_confirmable')
|
|
637
800
|
throw new IntentNotConfirmableError(String(created.id));
|
|
801
|
+
if (typeof s.reason === 'string' && PRESET_REFUSAL_REASONS.has(s.reason)) {
|
|
802
|
+
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));
|
|
803
|
+
}
|
|
638
804
|
if (s.reason === 'processor_refused') {
|
|
639
805
|
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
806
|
}
|
|
@@ -718,6 +884,11 @@ export class VaultClient {
|
|
|
718
884
|
return await this.post('/v2/checkout/authorizations', payload, signal, stopRetries);
|
|
719
885
|
}
|
|
720
886
|
catch (err) {
|
|
887
|
+
if (err instanceof CheckoutApiError && err.code && err.status === 403 && presetOf(err.details.preset)) {
|
|
888
|
+
// A company preset refused this purchase before a row existed.
|
|
889
|
+
const d = err.details;
|
|
890
|
+
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));
|
|
891
|
+
}
|
|
721
892
|
if (err instanceof CheckoutApiError && err.code === 'amount_mismatch') {
|
|
722
893
|
const d = err.details;
|
|
723
894
|
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');
|
|
@@ -804,6 +975,19 @@ export class VaultClient {
|
|
|
804
975
|
return this.call(path, { signal });
|
|
805
976
|
}
|
|
806
977
|
}
|
|
978
|
+
function executionMetadata(state, authorizationId, previous = {}) {
|
|
979
|
+
if (state.execution_mode === undefined)
|
|
980
|
+
return previous;
|
|
981
|
+
if (state.execution_mode !== 'user_approval' && state.execution_mode !== 'autopilot')
|
|
982
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'execution_mode_unrecognized');
|
|
983
|
+
if (state.execution_mode === 'user_approval')
|
|
984
|
+
return { executionMode: 'user_approval' };
|
|
985
|
+
if (typeof state.grant_id !== 'string' || !/^apg_[A-Za-z0-9_-]{1,128}$/.test(state.grant_id))
|
|
986
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_grant_unconfirmed');
|
|
987
|
+
if (previous.grantId && previous.grantId !== state.grant_id)
|
|
988
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_grant_changed');
|
|
989
|
+
return { executionMode: 'autopilot', grantId: state.grant_id };
|
|
990
|
+
}
|
|
807
991
|
/**
|
|
808
992
|
* Forward only the headers the merchant needs to accept the replay. Everything
|
|
809
993
|
* else (cookies, UA, tracing) is dropped so we transmit as little as possible.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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, RazorpayProcessorError, } 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
|
+
export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, ExecutionMetadata, } from './client.js';
|
|
3
3
|
export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
|
|
4
4
|
export { CheckoutAttachmentError } from './attachment.js';
|
|
5
5
|
export type { CdpLike, AttachOptions, CorsOutcome } from './cdp.js';
|