@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.
@@ -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
- /** Retire only a pre-replay authorization. A 409 or missing response remains unknown. */
507
- cancelAuthorization(authorizationId: string): Promise<{
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: 'merchant_request_aborted';
538
+ reason: RuntimeCancelReason;
511
539
  cancelled: true;
512
- processor_request_started: false;
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
- /** Retire only a pre-replay authorization. A 409 or missing response remains unknown. */
951
- async cancelAuthorization(authorizationId) {
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
- const result = await this.post(`/v2/checkout/authorizations/${authorizationId}/cancel`, {}, AbortSignal.timeout(5_000));
955
- if (result?.id !== authorizationId || result.status !== 'declined'
956
- || result.reason !== 'merchant_request_aborted' || result.cancelled !== true
957
- || result.processor_request_started !== false) {
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: 'merchant_request_aborted',
961
- cancelled: true, processor_request_started: false };
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';
@@ -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)
@@ -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;
@@ -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.15.1",
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",