@agent-cards/checkout 0.15.1 → 0.16.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 +4 -0
- package/README.md +14 -5
- package/dist/cdp.d.ts +38 -1
- package/dist/cdp.js +912 -75
- package/dist/checkout-com.generated.d.ts +4 -0
- package/dist/checkout-com.generated.js +183 -0
- package/dist/client.d.ts +32 -4
- package/dist/client.js +45 -9
- package/dist/index.d.ts +1 -1
- package/dist/lifecycle.d.ts +25 -0
- package/dist/lifecycle.js +34 -0
- package/dist/preparation.d.ts +1 -1
- package/dist/preparation.js +2 -2
- package/dist/prepared-processor.d.ts +2 -2
- package/dist/prepared-processor.js +7 -2
- package/package.json +3 -3
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
declare function checkoutComPreparationUrl(environment: any): any;
|
|
2
|
+
declare function checkoutComEnvironment(url: any): "production" | "sandbox" | undefined;
|
|
3
|
+
declare function isPreparedCheckoutComRequest(url: any, method: any, body: any, environment: any, headers: any): boolean;
|
|
4
|
+
export { checkoutComEnvironment, checkoutComPreparationUrl, isPreparedCheckoutComRequest };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
+
// artifact-sha256: aaa2f76129db849a23bf8bb44043f5cff2b45e853b16b5de4d61a1f6a10cd392
|
|
4
|
+
// src/checkout-com.js
|
|
5
|
+
var ENDPOINTS = Object.freeze({
|
|
6
|
+
production: ["https://api.checkout.com/tokens", "https://card-acquisition-gateway.checkout.com/tokens"],
|
|
7
|
+
sandbox: ["https://api.sandbox.checkout.com/tokens", "https://card-acquisition-gateway.sandbox.checkout.com/tokens"]
|
|
8
|
+
});
|
|
9
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
var own = (value, name) => Object.prototype.hasOwnProperty.call(value, name);
|
|
11
|
+
var keys = (value, allowed) => record(value) && Object.keys(value).every((name) => allowed.includes(name));
|
|
12
|
+
var exact = (pattern, value) => typeof value === "string" && pattern.exec(value)?.[0] === value;
|
|
13
|
+
var text = (value, max = 1024) => typeof value === "string" && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
14
|
+
var digits = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max && !/[^0-9]/.test(value);
|
|
15
|
+
function readBody(body) {
|
|
16
|
+
if (typeof body !== "string" || !body || body.length > 65536)
|
|
17
|
+
return null;
|
|
18
|
+
let at = 0, nodes = 0;
|
|
19
|
+
const fail = () => {
|
|
20
|
+
throw new Error("invalid_checkout_com_json");
|
|
21
|
+
};
|
|
22
|
+
const space = () => {
|
|
23
|
+
while (" \r\n".includes(body[at] ?? "\0"))
|
|
24
|
+
at++;
|
|
25
|
+
};
|
|
26
|
+
const string = () => {
|
|
27
|
+
space();
|
|
28
|
+
const start = at;
|
|
29
|
+
if (body[at++] !== '"')
|
|
30
|
+
fail();
|
|
31
|
+
while (at < body.length) {
|
|
32
|
+
const char = body[at++];
|
|
33
|
+
if (char === "\\") {
|
|
34
|
+
at++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (char === '"')
|
|
38
|
+
return JSON.parse(body.slice(start, at));
|
|
39
|
+
}
|
|
40
|
+
return fail();
|
|
41
|
+
};
|
|
42
|
+
const value = (depth) => {
|
|
43
|
+
if (depth > 12 || ++nodes > 1e3)
|
|
44
|
+
fail();
|
|
45
|
+
space();
|
|
46
|
+
const char = body[at];
|
|
47
|
+
if (char === '"')
|
|
48
|
+
return string();
|
|
49
|
+
if (char === "{") {
|
|
50
|
+
at++;
|
|
51
|
+
const result = /* @__PURE__ */ Object.create(null), seen = /* @__PURE__ */ new Set();
|
|
52
|
+
space();
|
|
53
|
+
if (body[at] === "}") {
|
|
54
|
+
at++;
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
while (true) {
|
|
58
|
+
const key = string();
|
|
59
|
+
if (seen.has(key) || ["__proto__", "constructor", "prototype"].includes(key))
|
|
60
|
+
fail();
|
|
61
|
+
seen.add(key);
|
|
62
|
+
space();
|
|
63
|
+
if (body[at++] !== ":")
|
|
64
|
+
fail();
|
|
65
|
+
result[key] = value(depth + 1);
|
|
66
|
+
space();
|
|
67
|
+
const next = body[at++];
|
|
68
|
+
if (next === "}")
|
|
69
|
+
return result;
|
|
70
|
+
if (next !== ",")
|
|
71
|
+
fail();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (char === "[") {
|
|
75
|
+
at++;
|
|
76
|
+
const result = [];
|
|
77
|
+
space();
|
|
78
|
+
if (body[at] === "]") {
|
|
79
|
+
at++;
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
while (true) {
|
|
83
|
+
result.push(value(depth + 1));
|
|
84
|
+
space();
|
|
85
|
+
const next = body[at++];
|
|
86
|
+
if (next === "]")
|
|
87
|
+
return result;
|
|
88
|
+
if (next !== ",")
|
|
89
|
+
fail();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const literal of ["true", "false", "null"]) {
|
|
93
|
+
if (body.startsWith(literal, at)) {
|
|
94
|
+
at += literal.length;
|
|
95
|
+
return JSON.parse(literal);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const number = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(body.slice(at));
|
|
99
|
+
if (!number)
|
|
100
|
+
fail();
|
|
101
|
+
at += number[0].length;
|
|
102
|
+
const parsed = Number(number[0]);
|
|
103
|
+
if (!Number.isFinite(parsed))
|
|
104
|
+
fail();
|
|
105
|
+
return parsed;
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
const parsed = value(0);
|
|
109
|
+
space();
|
|
110
|
+
return at === body.length && record(parsed) ? parsed : null;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function checkoutComPreparationUrl(environment) {
|
|
117
|
+
if (environment !== "production" && environment !== "sandbox")
|
|
118
|
+
throw new Error("unsupported_preparation_environment");
|
|
119
|
+
return ENDPOINTS[environment][1];
|
|
120
|
+
}
|
|
121
|
+
function checkoutComEnvironment(url) {
|
|
122
|
+
if (ENDPOINTS.production.includes(url))
|
|
123
|
+
return "production";
|
|
124
|
+
if (ENDPOINTS.sandbox.includes(url))
|
|
125
|
+
return "sandbox";
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
var ADDRESS_TEXT = ["address_line1", "address_line2", "city", "state", "zip", "name"];
|
|
129
|
+
function billing(value) {
|
|
130
|
+
if (!keys(value, [...ADDRESS_TEXT, "country", "phone"]) || ADDRESS_TEXT.some((key) => own(value, key) && !text(value[key])))
|
|
131
|
+
return false;
|
|
132
|
+
if (own(value, "country") && !exact(/^[A-Za-z]{2}$/, value.country))
|
|
133
|
+
return false;
|
|
134
|
+
return !own(value, "phone") || keys(value.phone, ["country_code", "number"]) && Object.values(value.phone).every((value2) => text(value2, 64));
|
|
135
|
+
}
|
|
136
|
+
function guestCard(value) {
|
|
137
|
+
if (!keys(value, ["type", "number", "expiry_month", "expiry_year", "cvv", "name", "billing_address", "consumer_wallet"]) || value.type !== "card" || !digits(value.number, 12, 19) || !digits(value.cvv, 3, 4) || !Number.isInteger(value.expiry_month) || value.expiry_month < 1 || value.expiry_month > 12 || !Number.isInteger(value.expiry_year) || value.expiry_year < 2e3 || value.expiry_year > 9999 || own(value, "name") && !text(value.name) || own(value, "billing_address") && !billing(value.billing_address))
|
|
138
|
+
return false;
|
|
139
|
+
if (own(value, "consumer_wallet")) {
|
|
140
|
+
const wallet = value.consumer_wallet;
|
|
141
|
+
if (!keys(wallet, ["settings"]) || !keys(wallet.settings, ["preferred_locale"]) || !exact(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/, wallet.settings.preferred_locale))
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
var BROWSER_HEADERS = /* @__PURE__ */ new Set([
|
|
147
|
+
"accept",
|
|
148
|
+
"accept-language",
|
|
149
|
+
"accept-encoding",
|
|
150
|
+
"content-length",
|
|
151
|
+
"origin",
|
|
152
|
+
"referer",
|
|
153
|
+
"user-agent",
|
|
154
|
+
"host",
|
|
155
|
+
"connection",
|
|
156
|
+
"cache-control",
|
|
157
|
+
"pragma",
|
|
158
|
+
"priority",
|
|
159
|
+
"dnt",
|
|
160
|
+
"sec-fetch-dest",
|
|
161
|
+
"sec-fetch-mode",
|
|
162
|
+
"sec-fetch-site",
|
|
163
|
+
"sec-fetch-user"
|
|
164
|
+
]);
|
|
165
|
+
function guestHeaders(headers) {
|
|
166
|
+
if (!record(headers))
|
|
167
|
+
return false;
|
|
168
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
169
|
+
let bytes = 0;
|
|
170
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
171
|
+
const lower = name.toLowerCase();
|
|
172
|
+
if (parsed.size >= 64 || parsed.has(lower) || !exact(/^[A-Za-z0-9-]+$/, name) || !text(value, 8192) || (bytes += name.length + value.length) > 32768)
|
|
173
|
+
return false;
|
|
174
|
+
if (!["content-type", "authorization"].includes(lower) && !BROWSER_HEADERS.has(lower) && !exact(/^sec-ch-ua(?:-[a-z0-9-]+)?$/, lower))
|
|
175
|
+
return false;
|
|
176
|
+
parsed.set(lower, value);
|
|
177
|
+
}
|
|
178
|
+
return exact(/^application\/json(?:;\s*charset=utf-8)?$/i, parsed.get("content-type")) && exact(/^(?:Bearer )?pk_[A-Za-z0-9_-]{1,256}\$?$/, parsed.get("authorization"));
|
|
179
|
+
}
|
|
180
|
+
function isPreparedCheckoutComRequest(url, method, body, environment, headers) {
|
|
181
|
+
return (environment === "production" || environment === "sandbox") && method === "POST" && checkoutComEnvironment(url) === environment && guestHeaders(headers) && guestCard(readBody(body));
|
|
182
|
+
}
|
|
183
|
+
export { checkoutComEnvironment, checkoutComPreparationUrl, isPreparedCheckoutComRequest };
|
package/dist/client.d.ts
CHANGED
|
@@ -248,6 +248,9 @@ export declare class ApprovalTimeoutError extends Error {
|
|
|
248
248
|
export declare class CheckoutCancelledError extends Error {
|
|
249
249
|
constructor();
|
|
250
250
|
}
|
|
251
|
+
/** The reasons an org runtime may stamp when it retires an authorization; see VaultClient.cancelAuthorization. */
|
|
252
|
+
export type RuntimeCancelReason = 'merchant_request_aborted' | 'merchant_never_retried';
|
|
253
|
+
export declare const RUNTIME_CANCEL_REASONS: readonly RuntimeCancelReason[];
|
|
251
254
|
/** The payment may have reached the processor. Reconcile the merchant order before any new attempt. */
|
|
252
255
|
export declare class PaymentOutcomeUnknownError extends Error {
|
|
253
256
|
authorizationId: string | null;
|
|
@@ -479,6 +482,14 @@ export declare class VaultClient {
|
|
|
479
482
|
syncRegistry(): Promise<void>;
|
|
480
483
|
/** True when this request is a card tokenization we can take over. */
|
|
481
484
|
isCardRequest(url: string, method?: string): boolean;
|
|
485
|
+
/**
|
|
486
|
+
* How the card would reach the processor on this request (`token`, `cse`
|
|
487
|
+
* or `hosted_form`; absent on the entry means `token`), or null when the
|
|
488
|
+
* registry does not recognize it. The adapters read it to decide whether an
|
|
489
|
+
* approval outlives the page's own request: a hosted form is a navigation
|
|
490
|
+
* and cannot.
|
|
491
|
+
*/
|
|
492
|
+
checkoutModeOf(url: string, method?: string): CheckoutMode | null;
|
|
482
493
|
/**
|
|
483
494
|
* Glob url patterns covering every host the CURRENT registry can send a card
|
|
484
495
|
* to — what a raw CDP connection has to hand `Fetch.enable` before any card
|
|
@@ -503,13 +514,30 @@ export declare class VaultClient {
|
|
|
503
514
|
authorize(input: AuthorizeInput): Promise<ReplayResponse>;
|
|
504
515
|
/** A lost bind acknowledgement must never resume the request. Recover metadata only for safe cleanup. */
|
|
505
516
|
private retireUncertainPreparation;
|
|
506
|
-
/**
|
|
507
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Retire an authorization the runtime is done with. A 409 or missing
|
|
519
|
+
* response remains unknown.
|
|
520
|
+
*
|
|
521
|
+
* `merchant_request_aborted` (the default): the merchant request is gone
|
|
522
|
+
* and the row must still be awaiting the person with no replay started; the
|
|
523
|
+
* acknowledgement carries `processor_request_started: false`.
|
|
524
|
+
*
|
|
525
|
+
* `merchant_never_retried`: the page abandoned its request while the person
|
|
526
|
+
* decided, the adapter kept the approval for the page's retry, and none
|
|
527
|
+
* came. The row may already be `approved` (a token or ciphertext minted on
|
|
528
|
+
* the device that this runtime handed to no request), so
|
|
529
|
+
* `processor_request_started` says whether the processor was asked; the API
|
|
530
|
+
* stamps this reason only where no charge can have been made and answers
|
|
531
|
+
* 409 otherwise. The acknowledgement echoes the reason the row actually
|
|
532
|
+
* carries: a row retired earlier under the other runtime reason answers
|
|
533
|
+
* with that one.
|
|
534
|
+
*/
|
|
535
|
+
cancelAuthorization(authorizationId: string, reason?: RuntimeCancelReason): Promise<{
|
|
508
536
|
id: string;
|
|
509
537
|
status: 'declined';
|
|
510
|
-
reason:
|
|
538
|
+
reason: RuntimeCancelReason;
|
|
511
539
|
cancelled: true;
|
|
512
|
-
processor_request_started:
|
|
540
|
+
processor_request_started: boolean;
|
|
513
541
|
}>;
|
|
514
542
|
/**
|
|
515
543
|
* POST the create, with two typed twists: a 502 `amount_unverifiable`
|
package/dist/client.js
CHANGED
|
@@ -54,6 +54,7 @@ export class ApprovalTimeoutError extends Error {
|
|
|
54
54
|
export class CheckoutCancelledError extends Error {
|
|
55
55
|
constructor() { super('checkout cancelled locally before authorization creation'); this.name = 'CheckoutCancelledError'; }
|
|
56
56
|
}
|
|
57
|
+
export const RUNTIME_CANCEL_REASONS = ['merchant_request_aborted', 'merchant_never_retried'];
|
|
57
58
|
/** The payment may have reached the processor. Reconcile the merchant order before any new attempt. */
|
|
58
59
|
export class PaymentOutcomeUnknownError extends Error {
|
|
59
60
|
authorizationId;
|
|
@@ -411,6 +412,19 @@ export class VaultClient {
|
|
|
411
412
|
isCardRequest(url, method = 'POST') {
|
|
412
413
|
return method.toUpperCase() === 'POST' && findRecognizer(url, this.registry) !== null;
|
|
413
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* How the card would reach the processor on this request (`token`, `cse`
|
|
417
|
+
* or `hosted_form`; absent on the entry means `token`), or null when the
|
|
418
|
+
* registry does not recognize it. The adapters read it to decide whether an
|
|
419
|
+
* approval outlives the page's own request: a hosted form is a navigation
|
|
420
|
+
* and cannot.
|
|
421
|
+
*/
|
|
422
|
+
checkoutModeOf(url, method = 'POST') {
|
|
423
|
+
if (method.toUpperCase() !== 'POST')
|
|
424
|
+
return null;
|
|
425
|
+
const rec = findRecognizer(url, this.registry);
|
|
426
|
+
return rec ? rec.mode ?? 'token' : null;
|
|
427
|
+
}
|
|
414
428
|
/**
|
|
415
429
|
* Glob url patterns covering every host the CURRENT registry can send a card
|
|
416
430
|
* to — what a raw CDP connection has to hand `Fetch.enable` before any card
|
|
@@ -577,7 +591,7 @@ export class VaultClient {
|
|
|
577
591
|
throw new CheckoutPreparationError(preparation.id, 'expired');
|
|
578
592
|
if (input.user !== preparation.user || input.merchant !== preparation.merchant || (typeof input.amount === 'number' && input.amount !== preparation.amount) || (typeof input.amount === 'string' && !validAmountInput(input.amount))
|
|
579
593
|
|| input.currency?.toLowerCase() !== preparation.currency || input.cardId !== preparation.cardId
|
|
580
|
-
|| !matchesPreparedRequest(preparation.psp, preparation.environment, input.request.url, input.request.method ?? 'POST', input.request.body))
|
|
594
|
+
|| !matchesPreparedRequest(preparation.psp, preparation.environment, input.request.url, input.request.method ?? 'POST', input.request.body, input.request.headers))
|
|
581
595
|
throw new CheckoutPreparationError(preparation.id, 'checkout_changed');
|
|
582
596
|
}
|
|
583
597
|
if (input.signal?.aborted)
|
|
@@ -947,18 +961,40 @@ export class VaultClient {
|
|
|
947
961
|
await this.cancelAuthorization(id).catch(() => { });
|
|
948
962
|
return id;
|
|
949
963
|
}
|
|
950
|
-
/**
|
|
951
|
-
|
|
964
|
+
/**
|
|
965
|
+
* Retire an authorization the runtime is done with. A 409 or missing
|
|
966
|
+
* response remains unknown.
|
|
967
|
+
*
|
|
968
|
+
* `merchant_request_aborted` (the default): the merchant request is gone
|
|
969
|
+
* and the row must still be awaiting the person with no replay started; the
|
|
970
|
+
* acknowledgement carries `processor_request_started: false`.
|
|
971
|
+
*
|
|
972
|
+
* `merchant_never_retried`: the page abandoned its request while the person
|
|
973
|
+
* decided, the adapter kept the approval for the page's retry, and none
|
|
974
|
+
* came. The row may already be `approved` (a token or ciphertext minted on
|
|
975
|
+
* the device that this runtime handed to no request), so
|
|
976
|
+
* `processor_request_started` says whether the processor was asked; the API
|
|
977
|
+
* stamps this reason only where no charge can have been made and answers
|
|
978
|
+
* 409 otherwise. The acknowledgement echoes the reason the row actually
|
|
979
|
+
* carries: a row retired earlier under the other runtime reason answers
|
|
980
|
+
* with that one.
|
|
981
|
+
*/
|
|
982
|
+
async cancelAuthorization(authorizationId, reason = 'merchant_request_aborted') {
|
|
952
983
|
if (!/^cauth_[A-Za-z0-9_-]{1,128}$/.test(authorizationId))
|
|
953
984
|
throw new Error('Invalid authorization ID.');
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
985
|
+
if (!RUNTIME_CANCEL_REASONS.includes(reason))
|
|
986
|
+
throw new Error('Unsupported cancellation reason.');
|
|
987
|
+
const result = await this.post(`/v2/checkout/authorizations/${authorizationId}/cancel`, reason === 'merchant_request_aborted' ? {} : { reason }, AbortSignal.timeout(5_000));
|
|
988
|
+
const recorded = result?.reason;
|
|
989
|
+
if (result?.id !== authorizationId || result.status !== 'declined' || result.cancelled !== true
|
|
990
|
+
|| typeof recorded !== 'string' || !RUNTIME_CANCEL_REASONS.includes(recorded)
|
|
991
|
+
|| typeof result.processor_request_started !== 'boolean'
|
|
992
|
+
// The plain cancellation is only ever confirmed before the card moved.
|
|
993
|
+
|| (recorded === 'merchant_request_aborted' && result.processor_request_started !== false)) {
|
|
958
994
|
throw new PaymentOutcomeUnknownError(authorizationId, 'authorization_cancel_unconfirmed');
|
|
959
995
|
}
|
|
960
|
-
return { id: authorizationId, status: 'declined', reason:
|
|
961
|
-
cancelled: true, processor_request_started:
|
|
996
|
+
return { id: authorizationId, status: 'declined', reason: recorded,
|
|
997
|
+
cancelled: true, processor_request_started: result.processor_request_started };
|
|
962
998
|
}
|
|
963
999
|
/**
|
|
964
1000
|
* POST the create, with two typed twists: a 502 `amount_unverifiable`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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';
|
|
2
|
+
export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, RuntimeCancelReason, 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';
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -94,6 +94,31 @@ export declare class CheckoutLifecycle implements CheckoutController {
|
|
|
94
94
|
cancel(): void;
|
|
95
95
|
/** The exact browser request is gone; a late approval cannot reopen it. */
|
|
96
96
|
merchantRequestAborted(): void;
|
|
97
|
+
/**
|
|
98
|
+
* The page abandoned its card request (its own script timed out) while
|
|
99
|
+
* the cardholder is still deciding. The approval stays pending and the
|
|
100
|
+
* page's next matching request will be answered from it, so the status
|
|
101
|
+
* stays `awaiting_approval`; the reason says the page has to ask again.
|
|
102
|
+
* Not a hold: nothing is refused, and no card or token has moved.
|
|
103
|
+
*/
|
|
104
|
+
merchantRequestLost(): void;
|
|
105
|
+
/**
|
|
106
|
+
* The cardholder approved, and there is no live page request to hand the
|
|
107
|
+
* answer to: the page gave up on its own request while they decided.
|
|
108
|
+
* `ready_to_submit` is the state a prepared checkout uses for "consent in
|
|
109
|
+
* hand, click Pay": the application's next Pay action produces the request
|
|
110
|
+
* this approval answers, with no second prompt on the phone.
|
|
111
|
+
*/
|
|
112
|
+
awaitingMerchantRetry(authorizationId: string): void;
|
|
113
|
+
/**
|
|
114
|
+
* The page never asked again inside the retry wait and the API confirmed
|
|
115
|
+
* the approval is retired: `declined`, like any other approval that ended
|
|
116
|
+
* with nothing charged, and not held, because the next card request is a
|
|
117
|
+
* new question (the cardholder is told to ask their agent to try again).
|
|
118
|
+
* A retirement the API refused or did not confirm is an unknown outcome and
|
|
119
|
+
* is reported through failed() instead, which holds.
|
|
120
|
+
*/
|
|
121
|
+
merchantNeverRetried(authorizationId: string | null): void;
|
|
97
122
|
unsupported(): void;
|
|
98
123
|
prepareHandoff(replay: ReplayResponse, requestUrl: string): void;
|
|
99
124
|
handedOff(replay: ReplayResponse): void;
|
package/dist/lifecycle.js
CHANGED
|
@@ -79,6 +79,40 @@ export class CheckoutLifecycle {
|
|
|
79
79
|
this.held = true;
|
|
80
80
|
this.set({ ...this.state, status: 'outcome_unknown', reason: 'merchant_request_aborted' });
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The page abandoned its card request (its own script timed out) while
|
|
84
|
+
* the cardholder is still deciding. The approval stays pending and the
|
|
85
|
+
* page's next matching request will be answered from it, so the status
|
|
86
|
+
* stays `awaiting_approval`; the reason says the page has to ask again.
|
|
87
|
+
* Not a hold: nothing is refused, and no card or token has moved.
|
|
88
|
+
*/
|
|
89
|
+
merchantRequestLost() {
|
|
90
|
+
this.set({ ...this.state, reason: 'merchant_request_lost' });
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The cardholder approved, and there is no live page request to hand the
|
|
94
|
+
* answer to: the page gave up on its own request while they decided.
|
|
95
|
+
* `ready_to_submit` is the state a prepared checkout uses for "consent in
|
|
96
|
+
* hand, click Pay": the application's next Pay action produces the request
|
|
97
|
+
* this approval answers, with no second prompt on the phone.
|
|
98
|
+
*/
|
|
99
|
+
awaitingMerchantRetry(authorizationId) {
|
|
100
|
+
this.set({ status: 'ready_to_submit', authorizationId, reason: 'awaiting_merchant_retry',
|
|
101
|
+
...(this.state.preparationId ? { preparationId: this.state.preparationId } : {}) });
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The page never asked again inside the retry wait and the API confirmed
|
|
105
|
+
* the approval is retired: `declined`, like any other approval that ended
|
|
106
|
+
* with nothing charged, and not held, because the next card request is a
|
|
107
|
+
* new question (the cardholder is told to ask their agent to try again).
|
|
108
|
+
* A retirement the API refused or did not confirm is an unknown outcome and
|
|
109
|
+
* is reported through failed() instead, which holds.
|
|
110
|
+
*/
|
|
111
|
+
merchantNeverRetried(authorizationId) {
|
|
112
|
+
if (this.cancelled)
|
|
113
|
+
return;
|
|
114
|
+
this.set({ ...this.state, authorizationId, status: 'declined', reason: 'merchant_never_retried' });
|
|
115
|
+
}
|
|
82
116
|
unsupported() {
|
|
83
117
|
this.held = true;
|
|
84
118
|
if (this.state.authorizationId)
|
package/dist/preparation.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare class PreparationGate {
|
|
|
15
15
|
constructor(opts: AttachOptions, lifecycle: CheckoutLifecycle, readDocumentUrl: () => Promise<string>);
|
|
16
16
|
private prepare;
|
|
17
17
|
/** Called for every recognized card mutation, before any await or local retry guard. */
|
|
18
|
-
claim(requestUrl: string, requestBody?: string | null): PreparedCheckout | undefined;
|
|
18
|
+
claim(requestUrl: string, requestBody?: string | null, requestHeaders?: Record<string, string>): PreparedCheckout | undefined;
|
|
19
19
|
assertDocument(): Promise<void>;
|
|
20
20
|
private readDocument;
|
|
21
21
|
isEngaged(): boolean;
|
package/dist/preparation.js
CHANGED
|
@@ -92,7 +92,7 @@ export class PreparationGate {
|
|
|
92
92
|
// Keep the signal listener after ready: caller cancellation retires the handle too.
|
|
93
93
|
}
|
|
94
94
|
/** Called for every recognized card mutation, before any await or local retry guard. */
|
|
95
|
-
claim(requestUrl, requestBody) {
|
|
95
|
+
claim(requestUrl, requestBody, requestHeaders) {
|
|
96
96
|
this.observedRequest = true;
|
|
97
97
|
if (this.state === 'unused')
|
|
98
98
|
return undefined;
|
|
@@ -101,7 +101,7 @@ export class PreparationGate {
|
|
|
101
101
|
throw new CheckoutPreparationError(this.prepared?.id ?? null, 'already_used_or_unavailable');
|
|
102
102
|
}
|
|
103
103
|
const prepared = this.prepared;
|
|
104
|
-
if (Date.parse(prepared.expiresAt) <= Date.now() || !matchesPreparedRequest(prepared.psp, prepared.environment, requestUrl, 'POST', requestBody)) {
|
|
104
|
+
if (Date.parse(prepared.expiresAt) <= Date.now() || !matchesPreparedRequest(prepared.psp, prepared.environment, requestUrl, 'POST', requestBody, requestHeaders)) {
|
|
105
105
|
const reason = Date.parse(prepared.expiresAt) <= Date.now() ? 'expired' : 'checkout_changed';
|
|
106
106
|
this.invalidate(reason);
|
|
107
107
|
throw new CheckoutPreparationError(prepared.id, reason);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen';
|
|
1
|
+
export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com';
|
|
2
2
|
export type PreparationEnvironment = 'production' | 'sandbox' | 'shared';
|
|
3
3
|
/** How the approved card reaches the processor: the device's own request (token), or ciphertext the device produces for this browser to send (cse, Adyen). */
|
|
4
4
|
export type PreparationMode = 'token' | 'cse';
|
|
@@ -7,4 +7,4 @@ export declare function preparationMode(psp: string): PreparationMode;
|
|
|
7
7
|
export declare function validPreparationEnvironment(psp: string, environment: string): boolean;
|
|
8
8
|
export declare function preparationEndpoint(psp: PreparationProcessor, environment: PreparationEnvironment): string;
|
|
9
9
|
/** Processor identity and environment are part of the device's prior consent. */
|
|
10
|
-
export declare function matchesPreparedRequest(psp: PreparationProcessor, environment: PreparationEnvironment, requestUrl: string, method: string, body?: string | null): boolean;
|
|
10
|
+
export declare function matchesPreparedRequest(psp: PreparationProcessor, environment: PreparationEnvironment, requestUrl: string, method: string, body?: string | null, headers?: Record<string, string>): boolean;
|
|
@@ -2,6 +2,7 @@ import { adyenPreparationUrl, isPreparedAdyenRequest } from './adyen.generated.j
|
|
|
2
2
|
import { braintreeEnvironment, isPreparedBraintreeRequest, readTokenizationJson } from './braintree.js';
|
|
3
3
|
import { isPreparedRecurlyRequest } from './recurly.generated.js';
|
|
4
4
|
import { isPreparedSpreedlyRequest, isSpreedlyTokenRequest, SPREEDLY_TOKEN_ENDPOINT } from './spreedly.generated.js';
|
|
5
|
+
import { checkoutComPreparationUrl, isPreparedCheckoutComRequest } from './checkout-com.generated.js';
|
|
5
6
|
export function preparationMode(psp) {
|
|
6
7
|
return psp === 'adyen' ? 'cse' : 'token';
|
|
7
8
|
}
|
|
@@ -9,11 +10,13 @@ export function preparationMode(psp) {
|
|
|
9
10
|
export function validPreparationEnvironment(psp, environment) {
|
|
10
11
|
if (psp === 'bambora' || psp === 'mercado_pago' || psp === 'recurly' || psp === 'spreedly')
|
|
11
12
|
return environment === 'shared';
|
|
12
|
-
return ['square', 'braintree', 'worldpay', 'adyen'].includes(psp) && ['production', 'sandbox'].includes(environment);
|
|
13
|
+
return ['square', 'braintree', 'worldpay', 'adyen', 'checkout_com'].includes(psp) && ['production', 'sandbox'].includes(environment);
|
|
13
14
|
}
|
|
14
15
|
export function preparationEndpoint(psp, environment) {
|
|
15
16
|
if (!validPreparationEnvironment(psp, environment))
|
|
16
17
|
throw new Error('unsupported_preparation_processor');
|
|
18
|
+
if (psp === 'checkout_com')
|
|
19
|
+
return checkoutComPreparationUrl(environment);
|
|
17
20
|
if (psp === 'adyen')
|
|
18
21
|
return adyenPreparationUrl(environment);
|
|
19
22
|
if (psp === 'bambora')
|
|
@@ -73,9 +76,11 @@ function freshCardBody(psp, body) {
|
|
|
73
76
|
return true;
|
|
74
77
|
}
|
|
75
78
|
/** Processor identity and environment are part of the device's prior consent. */
|
|
76
|
-
export function matchesPreparedRequest(psp, environment, requestUrl, method, body) {
|
|
79
|
+
export function matchesPreparedRequest(psp, environment, requestUrl, method, body, headers) {
|
|
77
80
|
if (method.toUpperCase() !== 'POST' || !validPreparationEnvironment(psp, environment))
|
|
78
81
|
return false;
|
|
82
|
+
if (psp === 'checkout_com')
|
|
83
|
+
return isPreparedCheckoutComRequest(requestUrl, method, body ?? null, environment, headers);
|
|
79
84
|
if (psp === 'adyen')
|
|
80
85
|
return isPreparedAdyenRequest(requestUrl, method, body ?? null, environment);
|
|
81
86
|
if (psp === 'braintree')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-cards/checkout",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Let browser agents pay with the user's own card, without your infrastructure ever touching card data.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"_comment_build": "The public SDK keeps zero runtime dependencies. Its build vendors deterministic metadata and substitution artifacts from the internal payment core; TypeScript remains pinned.",
|
|
28
28
|
"build": "node ../payment-core/scripts/build.mjs && node scripts/generate-payment-core.mjs && npx -y -p typescript@5.9.3 tsc && node scripts/generate-preflight-contract.mjs",
|
|
29
29
|
"prepublishOnly": "pnpm build",
|
|
30
|
-
"test": "node test.mjs && node --test lifecycle.test.mjs merchant-abort.test.mjs preparation.test.mjs braintree.test.mjs autopilot.test.mjs stripe-checkout.test.mjs payment-core.test.mjs prepared-processor.test.mjs minimum-delay.test.mjs paysafe.test.mjs attachment.test.mjs mercado-checkout.test.mjs mercado-polling.test.mjs && node --test preflight-package.test.mjs preflight-collector.test.mjs && node --test kernel-native-qualification.test.mjs && node --test ../vault/scripts/recurly-validation/watch-duty-sdk-result.test.mjs",
|
|
31
|
-
"test:browser": "node browser.test.mjs && node stripe-browser.test.mjs && node preparation-browser.test.mjs && node owned-shop-browser.test.mjs && node spreedly-browser.test.mjs",
|
|
30
|
+
"test": "node test.mjs && node --test lifecycle.test.mjs merchant-abort.test.mjs preparation.test.mjs braintree.test.mjs autopilot.test.mjs stripe-checkout.test.mjs payment-core.test.mjs prepared-processor.test.mjs minimum-delay.test.mjs paysafe.test.mjs attachment.test.mjs worker-targets.test.mjs mercado-checkout.test.mjs mercado-polling.test.mjs && node --test preflight-package.test.mjs preflight-collector.test.mjs && node --test kernel-native-qualification.test.mjs && node --test ../vault/scripts/recurly-validation/watch-duty-sdk-result.test.mjs",
|
|
31
|
+
"test:browser": "node browser.test.mjs && node stripe-browser.test.mjs && node preparation-browser.test.mjs && node checkout-com-browser.test.mjs && node owned-shop-browser.test.mjs && node spreedly-browser.test.mjs && node worker-browser.test.mjs",
|
|
32
32
|
"check:payment-core": "node scripts/generate-payment-core.mjs --check",
|
|
33
33
|
"test:preflight": "node --test preflight-package.test.mjs preflight-collector.test.mjs kernel-native-qualification.test.mjs",
|
|
34
34
|
"test:preflight:browser": "node preflight-browser.test.mjs",
|