@agent-cards/sdk 0.23.3 → 0.23.5

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,214 @@
1
+ /**
2
+ * CyberSource Secure Acceptance's Silent Order POST, which this SDK refuses for every company.
3
+ *
4
+ * CyberSource's Secure Acceptance Checkout API, formerly Silent Order POST, takes the card from a form
5
+ * on the merchant's own page. The merchant's server signs the order fields, and the shopper's browser
6
+ * posts them, with the card fields left unsigned, to a /silent/ path on one of CyberSource's Secure
7
+ * Acceptance hosts: /silent/pay, /silent/token/create, /silent/token/update, and the iframe form of
8
+ * each under /silent/embedded/ (CyberSource's endpoint table, read 2026-10-08). Arrow Electronics'
9
+ * checkout (its public bundles, read 2026-10-08; no card was sent) builds that form once its server
10
+ * has signed the fields and submits it into a hidden iframe:
11
+ *
12
+ * <iframe name="secureAcceptanceiFrame" style="display:none">
13
+ * <form action="https://secureacceptance.cybersource.com/silent/embedded/token/create" method="post"
14
+ * target="secureAcceptanceiFrame">: access_key, profile_id, transaction_type=create_payment_token,
15
+ * amount=0 and the other signed fields with their signature, then card_type, card_number,
16
+ * card_expiry_date (MM-YYYY) and card_cvn
17
+ *
18
+ * No Vault mode takes this post over, so no company can pay it, and an SDK without this guard let the
19
+ * card the agent typed reach CyberSource. Both adapters arm /silent/ on the three hosts whatever the
20
+ * synced registry serves, over https and http, each host also spelled with the trailing dot of a fully
21
+ * qualified name, and the pre-claim gate (cybersourceSecureAcceptancePreClaim, pre-claim.ts) judges
22
+ * each request there, whether a fetch, an XHR or a form navigation of the page or of a frame sent it,
23
+ * by its body and by its URL's query (cybersourceSecureAcceptanceRequest):
24
+ * - a card_number that holds a value: stop it, so CyberSource never receives the card. A form
25
+ * navigation is answered with the stopped page (card-endpoint-page.ts), which names CyberSource,
26
+ * since an aborted page load renders nothing and invites another click on Pay; any other request
27
+ * fails. The checkout reads `unsupported` with reason `unsupported_cybersource_secure_acceptance`
28
+ * and holds, as every unsupported refusal does;
29
+ * - no card_number, or an empty one, as in a payment with a stored payment_token: continue untouched;
30
+ * - a body the adapter could not read: stop that request alone, since nothing proves it carries no card.
31
+ * A request the synced registry recognizes as a card request belongs to that recognizer instead; no
32
+ * build serves one today. Hosted Checkout, the same hosts without /silent/, takes the card on
33
+ * CyberSource's own page, and neither adapter arms it.
34
+ */
35
+ export const CYBERSOURCE_SECURE_ACCEPTANCE_REASON = 'unsupported_cybersource_secure_acceptance';
36
+ /** Where the stopped page says the card was going. */
37
+ export const CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT = 'CyberSource';
38
+ /** CyberSource's Secure Acceptance hosts: production, test, and production in India. */
39
+ export const CYBERSOURCE_SECURE_ACCEPTANCE_HOSTS = Object.freeze([
40
+ 'secureacceptance.cybersource.com', 'testsecureacceptance.cybersource.com', 'secureacceptance.in.cybersource.com',
41
+ ]);
42
+ /**
43
+ * CDP Fetch patterns for every /silent/ path on the three hosts, armed on every attachment, over https
44
+ * and over http, each host also with its trailing dot: Chrome sends a URL spelled either way unchanged,
45
+ * and DNS reads both spellings as the same host, so a page's form posted to either reaches CyberSource.
46
+ * The * before silent/ also pauses a path that doubles its slash, as a base URL that ends in a slash
47
+ * joined to /silent/pay writes it, which a server that merges slashes reads as /silent/pay. The gate
48
+ * judges each path with its slashes merged (isCybersourceSecureAcceptanceUrl), and anything else the *
49
+ * pauses, such as /billing/silent/pay, continues untouched. A pattern matches the canonical URL Chrome
50
+ * sends, whose host is lower case and whose standard port is dropped; no URL without silent/ is armed.
51
+ */
52
+ export const CYBERSOURCE_SECURE_ACCEPTANCE_PATTERNS = Object.freeze(CYBERSOURCE_SECURE_ACCEPTANCE_HOSTS.flatMap((host) => ['https', 'http'].flatMap((scheme) => [`${scheme}://${host}/*silent/*`, `${scheme}://${host}./*silent/*`])));
53
+ /**
54
+ * Whether this URL is a /silent/ path on one of the three hosts: https or http, the host with or without
55
+ * its trailing dot, on the scheme's standard port, with no credentials, and its path read with each run
56
+ * of slashes merged into one, as a server that merges slashes reads it.
57
+ */
58
+ export function isCybersourceSecureAcceptanceUrl(url) {
59
+ let parsed;
60
+ try {
61
+ parsed = new URL(url);
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ return (parsed.protocol === 'https:' || parsed.protocol === 'http:') && !parsed.port && !parsed.username && !parsed.password
67
+ && CYBERSOURCE_SECURE_ACCEPTANCE_HOSTS.includes(parsed.hostname.toLowerCase().replace(/\.$/, ''))
68
+ && parsed.pathname.replace(/\/{2,}/g, '/').startsWith('/silent/');
69
+ }
70
+ /** Methods that send no body: only the URL's query can carry a card on them. */
71
+ const BODILESS = new Set(['GET', 'HEAD', 'OPTIONS']);
72
+ /**
73
+ * What a request to a /silent/ path carries: 'card' when card_number holds a value in the URL's query,
74
+ * on any method, since a form whose method is GET puts its fields there; otherwise the body's verdict
75
+ * (cybersourceSecureAcceptanceBody) for a method that sends one, and 'no_card' for a GET, a HEAD or a
76
+ * CORS preflight.
77
+ */
78
+ export function cybersourceSecureAcceptanceRequest(url, method, body, contentType) {
79
+ let query = '';
80
+ try {
81
+ query = new URL(url).search;
82
+ }
83
+ catch {
84
+ query = '';
85
+ }
86
+ if (carriesCardNumber(formFields(query)))
87
+ return 'card';
88
+ return BODILESS.has(String(method).toUpperCase()) ? 'no_card' : cybersourceSecureAcceptanceBody(body, contentType);
89
+ }
90
+ /**
91
+ * What a Silent Order POST's body carries: 'card' when any reading of it finds a field named card_number
92
+ * that holds a value, 'no_card' when none does, and 'unreadable' for a body the adapter could not read
93
+ * (null), or a multipart body whose parts cannot be read and in which no reading finds a card, a
94
+ * multipart content type with no boundary included.
95
+ *
96
+ * The page writes both the bytes and their content type, and a server reads the bytes by that type, so
97
+ * the gate trusts no one reading. It reads every body as URL-encoded pairs (split on & and line by line,
98
+ * which also reads a plain-text form's name=value lines), as JSON at any depth, and as a multipart form:
99
+ * by its parts, when its content type names a boundary or it opens with a delimiter line and a part
100
+ * header, and by every part header that names card_number, wherever it sits. A card in any reading stops
101
+ * the post, so a body whose content type names one encoding and whose bytes hold another, a URL-encoded
102
+ * body that also parses as JSON, say, cannot carry the card past the gate.
103
+ */
104
+ export function cybersourceSecureAcceptanceBody(body, contentType) {
105
+ if (typeof body !== 'string')
106
+ return 'unreadable';
107
+ const delimiter = multipartDelimiter(body, typeof contentType === 'string' ? contentType : '');
108
+ const parts = delimiter === undefined ? [] : delimiter === null ? null : multipartFields(body, delimiter);
109
+ if (carriesCardNumber(formFields(body)) || carriesCardNumber(jsonFields(body)) || carriesCardNumber(parts ?? [])
110
+ || multipartNamesCardNumber(body, delimiter ?? undefined))
111
+ return 'card';
112
+ return parts === null ? 'unreadable' : 'no_card';
113
+ }
114
+ /**
115
+ * Whether a field named card_number holds a value. The name matches in any case and every occurrence
116
+ * counts; an empty value, or one of whitespace only, holds no card.
117
+ */
118
+ function carriesCardNumber(fields) {
119
+ return fields.some(([name, value]) => name.trim().toLowerCase() === 'card_number'
120
+ && value !== undefined && value !== null && !(typeof value === 'string' && value.trim() === ''));
121
+ }
122
+ /**
123
+ * The delimiter line of a multipart body: the one its content type names, or, whatever its content type
124
+ * says, the one it opens with when a part header follows. Undefined for a body that is not multipart,
125
+ * and null for a multipart content type that names no boundary.
126
+ */
127
+ function multipartDelimiter(body, contentType) {
128
+ if (/^\s*multipart\//i.test(contentType)) {
129
+ const boundary = /;\s*boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(contentType);
130
+ return boundary ? `--${boundary[1] ?? boundary[2]}` : null;
131
+ }
132
+ return /^(--[^\r\n]+)\r?\n[^\s:]+:/.exec(body)?.[1];
133
+ }
134
+ /**
135
+ * A multipart body's named parts, or null when a body with content never names its delimiter or one
136
+ * of its parts has no blank line after its headers. A part takes every name its headers give it.
137
+ */
138
+ function multipartFields(body, delimiter) {
139
+ if (!body.includes(delimiter))
140
+ return body.trim() === '' ? [] : null;
141
+ const fields = [];
142
+ for (const part of body.split(delimiter).slice(1)) {
143
+ if (part.startsWith('--'))
144
+ break; // the closing delimiter
145
+ const blank = /\r?\n\r?\n/.exec(part);
146
+ if (!blank)
147
+ return null;
148
+ const value = part.slice(blank.index + blank[0].length).replace(/\r?\n$/, '');
149
+ for (const name of headerNames(part.slice(0, blank.index)))
150
+ fields.push([name, value]);
151
+ }
152
+ return fields;
153
+ }
154
+ /** Every name a part's headers give it, quoted or bare, whichever header carries it. */
155
+ function headerNames(headers) {
156
+ return [...headers.matchAll(/;\s*name\s*=\s*(?:"([^"]*)"|([^;\s"]*))/gi)].map((match) => match[1] ?? match[2]);
157
+ }
158
+ /**
159
+ * Whether a part named card_number holds a value, read without trusting where the body splits into
160
+ * parts: each header line that names card_number, wherever it sits, opens a part whose value runs from
161
+ * the blank line after its headers to the next delimiter line (the body's own, or any line that opens
162
+ * with -- when it names none). A server that splits the parts elsewhere, at a delimiter written inside
163
+ * another part's value, say, finds no card this reading misses.
164
+ */
165
+ function multipartNamesCardNumber(body, delimiter) {
166
+ const lines = body.split(/\r?\n/);
167
+ const ends = (line) => (delimiter === undefined ? line.startsWith('--') : [delimiter, `${delimiter}--`].includes(line.trimEnd()));
168
+ for (let i = 0; i < lines.length; i++) {
169
+ if (!/^[^\s:]+:/.test(lines[i]) || !headerNames(lines[i]).some((name) => name.trim().toLowerCase() === 'card_number'))
170
+ continue;
171
+ let j = i + 1;
172
+ while (j < lines.length && lines[j].trim() !== '')
173
+ j++;
174
+ for (j++; j < lines.length && !ends(lines[j]); j++)
175
+ if (lines[j].trim() !== '')
176
+ return true;
177
+ }
178
+ return false;
179
+ }
180
+ /** Every key of a JSON body, at any depth, with its value; none for a body that is not JSON. */
181
+ function jsonFields(body) {
182
+ if (!/^\s*[[{]/.test(body))
183
+ return [];
184
+ let parsed;
185
+ try {
186
+ parsed = JSON.parse(body);
187
+ }
188
+ catch {
189
+ return [];
190
+ }
191
+ const fields = [];
192
+ const walk = (node, depth) => {
193
+ if (node === null || typeof node !== 'object' || depth > 64)
194
+ return;
195
+ for (const [name, value] of Object.entries(node)) {
196
+ fields.push([name, value]);
197
+ walk(value, depth + 1);
198
+ }
199
+ };
200
+ walk(parsed, 0);
201
+ return fields;
202
+ }
203
+ /**
204
+ * URL-encoded pairs, split on & as a server splits a form, and again line by line: a plain-text form
205
+ * sends name=value lines, and no raw line break survives URL encoding, so a line break in a URL-encoded
206
+ * body only comes from a page that wrote one there.
207
+ */
208
+ function formFields(body) {
209
+ const fields = [...new URLSearchParams(body)];
210
+ for (const line of body.split(/[\r\n]+/))
211
+ for (const pair of new URLSearchParams(line))
212
+ fields.push(pair);
213
+ return fields;
214
+ }
@@ -226,11 +226,14 @@ export declare class CheckoutLifecycle implements CheckoutController {
226
226
  * (`unsupported_payrails_card_payment` and the card-save refusal codes), a
227
227
  * Shiji STS request its gate refuses (shiji_sts_keep_on_file and the other
228
228
  * shiji-sts.ts reasons), a post to a processor's own card page that only
229
- * the cardholder's device can fill (processor_card_page: NMI's), or a card
229
+ * the cardholder's device can fill (processor_card_page: NMI's), a card
230
230
  * sent to PayPal's legacy Hosted Fields confirm, which no Vault mode takes
231
- * over (unsupported_paypal_hosted_fields, paypal-hosted-fields.ts).
231
+ * over (unsupported_paypal_hosted_fields, paypal-hosted-fields.ts), or a card
232
+ * a merchant's page posts to CyberSource Secure Acceptance's Silent Order
233
+ * POST, which no Vault mode takes over either
234
+ * (unsupported_cybersource_secure_acceptance, cybersource-secure-acceptance.ts).
232
235
  */
233
- unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported' | GmoUnsupportedReason | 'microform_unsupported' | 'juspay_request_unsupported' | 'merchant_card_endpoint_not_enabled' | 'merchant_card_endpoint_resend_refused' | 'creditguard_three_d_secure' | 'unsupported_primer_sdk' | 'unsupported_primer_request' | PayrailsUnsupportedReason | ShijiStsUnsupportedReason | 'processor_card_page' | 'unsupported_paypal_hosted_fields'): void;
236
+ unsupported(reason?: 'unrecognized_payment_endpoint' | 'merchant_profile_observe' | 'merchant_request_unsupported' | GmoUnsupportedReason | 'microform_unsupported' | 'juspay_request_unsupported' | 'merchant_card_endpoint_not_enabled' | 'merchant_card_endpoint_resend_refused' | 'creditguard_three_d_secure' | 'unsupported_primer_sdk' | 'unsupported_primer_request' | PayrailsUnsupportedReason | ShijiStsUnsupportedReason | 'processor_card_page' | 'unsupported_paypal_hosted_fields' | 'unsupported_cybersource_secure_acceptance'): void;
234
237
  /**
235
238
  * A card request that only a prepared checkout pays (an Adyen merchant
236
239
  * profile's, or a Fiserv card capture) arrived with no preparation on this
package/dist/lifecycle.js CHANGED
@@ -202,9 +202,12 @@ export class CheckoutLifecycle {
202
202
  * (`unsupported_payrails_card_payment` and the card-save refusal codes), a
203
203
  * Shiji STS request its gate refuses (shiji_sts_keep_on_file and the other
204
204
  * shiji-sts.ts reasons), a post to a processor's own card page that only
205
- * the cardholder's device can fill (processor_card_page: NMI's), or a card
205
+ * the cardholder's device can fill (processor_card_page: NMI's), a card
206
206
  * sent to PayPal's legacy Hosted Fields confirm, which no Vault mode takes
207
- * over (unsupported_paypal_hosted_fields, paypal-hosted-fields.ts).
207
+ * over (unsupported_paypal_hosted_fields, paypal-hosted-fields.ts), or a card
208
+ * a merchant's page posts to CyberSource Secure Acceptance's Silent Order
209
+ * POST, which no Vault mode takes over either
210
+ * (unsupported_cybersource_secure_acceptance, cybersource-secure-acceptance.ts).
208
211
  */
209
212
  unsupported(reason = 'unrecognized_payment_endpoint') {
210
213
  this.held = true;
@@ -17,7 +17,8 @@
17
17
  * the agent's dummy card never reaches the merchant and nothing is charged;
18
18
  * 'enabled' lets a prepared checkout pay it. The API can hold a profile in
19
19
  * 'observe' for a client but never enable it past this build: a profile is
20
- * 'enabled' only when both say so. Every profile ships in 'observe' today.
20
+ * 'enabled' only when both say so. Cinemark and Dick's own storefront ship
21
+ * 'enabled'; every other profile ships in 'observe'.
21
22
  */
22
23
  import { MERCHANT_PROFILE_RECOGNIZERS as SHARED_PROFILES, classifyDeclaredMerchantRequest as sharedClassifyDeclared, declaredTemplatedUrl as sharedDeclaredTemplatedUrl, declaredUrlGlobs as sharedDeclaredUrlGlobs, declaredUrlRelation as sharedDeclaredUrlRelation, declaredEndpointRefusal as sharedDeclaredEndpointRefusal, classifyMerchantRequest as sharedClassify, merchantBodyKeyPathSha256 as sharedKeyPathSha256, merchantProfileEnvironment as sharedEnvironment, merchantProfileRulesMatch as sharedRulesMatch, merchantProfileTemplatedUrl as sharedTemplatedUrl, merchantProfileUrlGlobs as sharedUrlGlobs, merchantProfileUrlRelation as sharedUrlRelation, sha256Hex as sharedSha256Hex, } from './adyen-merchant-hosted.generated.js';
23
24
  import { SubstitutionError, substituteMerchantHosted as sharedSubstitute } from './substitutions.generated.js';
@@ -12,7 +12,10 @@
12
12
  *
13
13
  * What this proves: the total is what the agent's browser session saw. It catches a
14
14
  * mistyped amount and a cart that changed under the agent; it cannot tell a merchant's
15
- * response from one the agent wrote, so every profile priced this way stays in observe.
15
+ * response from one the agent wrote. So a profile priced this way stays in observe unless
16
+ * Agentcard accepted that risk for it by name (Cinemark and Dick's own storefront, Oct 7,
17
+ * 2026), and then it pays only for the company Agentcard enables it for, and only after the
18
+ * cardholder approves each payment.
16
19
  *
17
20
  * What the SDK owes payment-core (the header of merchant-amount.js): every exchange of
18
21
  * the profile's shapes, projected, with the time it left; a wait for any such request
@@ -201,6 +201,7 @@ export declare const verifonePreClaim: PreClaimGate;
201
201
  export declare const fiservPreClaim: PreClaimGate;
202
202
  export declare const ekashuPreClaim: PreClaimGate;
203
203
  export declare const paypalHostedFieldsPreClaim: PreClaimGate;
204
+ export declare const cybersourceSecureAcceptancePreClaim: PreClaimGate;
204
205
  /** The processor gates, in order. A new family adds one entry here; the terminal
205
206
  * arbiter runs after all of them and is not part of this list. A gate appended
206
207
  * after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
package/dist/pre-claim.js CHANGED
@@ -16,7 +16,8 @@
16
16
  // card-saving bodies, WSPay's confirm URL, which also carries bodies with
17
17
  // no new card, Shiji STS's getTokenId and getCvvId, which also carry
18
18
  // requests the vault must never pay, and PayPal's legacy Hosted Fields
19
- // confirm, whose card no Vault mode takes over.
19
+ // confirm and CyberSource Secure Acceptance's Silent Order POST, whose card
20
+ // no Vault mode takes over.
20
21
  // The first three used to be
21
22
  // written inline in Fetch.requestPaused and again in the page.route handler, so
22
23
  // a decision could drift between the two adapters and a new family had to be
@@ -75,6 +76,7 @@ import { PAYRAILS_HOLDING_REASONS, PAYRAILS_SESSION_MESSAGE, isPayrailsHost, isP
75
76
  import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone.js';
76
77
  import { ekashuEntryUrl, isEkashuEntry, isEkashuHost } from './ekashu.js';
77
78
  import { PAYPAL_HOSTED_FIELDS_REASON, isPaypalHostedFieldsConfirm, paypalHostedFieldsBody } from './paypal-hosted-fields.js';
79
+ import { CYBERSOURCE_SECURE_ACCEPTANCE_REASON, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, cybersourceSecureAcceptanceRequest, isCybersourceSecureAcceptanceUrl, } from './cybersource-secure-acceptance.js';
78
80
  /**
79
81
  * The URL an event may name for a request. For an armed merchant profile's
80
82
  * endpoint or a sibling of it: the endpoint origin, the path TEMPLATE and the
@@ -169,14 +171,16 @@ function cardEndpointPausedBody(ctx) {
169
171
  }
170
172
  /**
171
173
  * Answer a paused card form navigation with the page that says Agentcard stopped it before
172
- * its card left. While an approval is outstanding, or once a payment for this checkout may
173
- * already exist, the page says a payment is in progress instead of that nothing was sent
174
- * (card-endpoint-page.ts refusedNavigationOutcome), and names that payment's authorization.
174
+ * its card left, naming where the card was going: a card endpoint profile's own merchant, or
175
+ * CyberSource for a Secure Acceptance post. While an approval is outstanding, or once a payment
176
+ * for this checkout may already exist, the page says a payment is in progress instead of that
177
+ * nothing was sent (card-endpoint-page.ts refusedNavigationOutcome), and names that payment's
178
+ * authorization.
175
179
  */
176
- async function stopNavigationBeforeSend(ctx, profile, reason) {
180
+ async function stopNavigationBeforeSend(ctx, merchant, reason) {
177
181
  const state = ctx.lifecycle.getState();
178
182
  const outcome = refusedNavigationOutcome({ awaitingApproval: ctx.awaitingApproval(), status: state.status });
179
- await ctx.effects.answer(cardEndpointStoppedPage({ outcome, reason, merchant: profile.merchant,
183
+ await ctx.effects.answer(cardEndpointStoppedPage({ outcome, reason, merchant,
180
184
  authorizationId: outcome === 'payment_in_progress' ? state.authorizationId : null }));
181
185
  return { kind: 'answer' };
182
186
  }
@@ -216,13 +220,13 @@ async function cardEndpointNavigationPreClaim(ctx, profile) {
216
220
  }
217
221
  if (navigation !== profile.navigation) {
218
222
  ctx.onEvent?.({ type: 'card_endpoint_refused', detail: { ...cardEndpointDetail(profile), reason: 'request_unsupported', refusal: `the card post navigates ${navigation === 'top' ? 'the page' : 'an iframe'}` } });
219
- return stopNavigationBeforeSend(ctx, profile, 'request_unsupported');
223
+ return stopNavigationBeforeSend(ctx, profile.merchant, 'request_unsupported');
220
224
  }
221
225
  if (!cardEndpointLive(profile)) {
222
226
  ctx.preparationGate.invalidate('unsupported_checkout');
223
227
  ctx.lifecycle.unsupported('merchant_card_endpoint_not_enabled');
224
228
  ctx.onEvent?.({ type: 'unsupported_checkout', detail: { ...cardEndpointDetail(profile), reason: 'merchant_card_endpoint_not_enabled' } });
225
- return stopNavigationBeforeSend(ctx, profile, 'not_enabled');
229
+ return stopNavigationBeforeSend(ctx, profile.merchant, 'not_enabled');
226
230
  }
227
231
  return { kind: 'pause', stripeStep: null, cardEndpoint: profile, navigation };
228
232
  }
@@ -1530,6 +1534,68 @@ async function paypalHostedFieldsUnreadable(ctx) {
1530
1534
  await ctx.effects.abort();
1531
1535
  return { kind: 'abort' };
1532
1536
  }
1537
+ // CyberSource Secure Acceptance's Silent Order POST (cybersource-secure-acceptance.ts). The merchant's
1538
+ // page posts the card the agent typed, beside the fields its server signed, to a /silent/ path on
1539
+ // CyberSource's Secure Acceptance hosts, which no Vault mode takes over, so every company's adapters
1540
+ // hand each request there to this gate, whether a fetch, an XHR or a form navigation sent it:
1541
+ // - a card_number that holds a value, in the body or in the URL's query, stops the request as an
1542
+ // unsupported checkout, reason `unsupported_cybersource_secure_acceptance`, with no claim, create or
1543
+ // prompt, so CyberSource never receives the card. A form navigation of the page or of a frame (Arrow
1544
+ // submits its form into a hidden iframe) is answered with the stopped page, as a card endpoint's form
1545
+ // is, since an aborted page load renders nothing and invites another click on Pay; any other request
1546
+ // fails. The attachment then reads `unsupported` and holds: the page's next post would carry the
1547
+ // card again;
1548
+ // - a body the adapter could not read stops that request alone, the same way, with a `blocked` event,
1549
+ // `unreadable_cybersource_secure_acceptance_request`, and does not hold the attachment: nothing
1550
+ // proves it carries no card;
1551
+ // - any other request (a payment with a stored payment_token and no card, a GET, a CORS preflight)
1552
+ // defers, and the arbiter continues it untouched.
1553
+ // A request the synced registry recognizes as a card request defers too, so the arbiter pauses it for
1554
+ // that recognizer; no build serves one today. A path outside /silent/ is not this gate's. Synchronous
1555
+ // until it must stop a request, like every gate. Exported for the gate registry's tests.
1556
+ export const cybersourceSecureAcceptancePreClaim = (ctx) => {
1557
+ const { url, method } = ctx.request;
1558
+ if (!isCybersourceSecureAcceptanceUrl(url) || ctx.vault.isCardRequest(url, method))
1559
+ return undefined;
1560
+ // A reader that throws (Playwright's postData() can) reads as a body the adapter could not read.
1561
+ const { body, contentType } = cardEndpointPausedBody(ctx);
1562
+ const carried = cybersourceSecureAcceptanceRequest(url, method, body, contentType);
1563
+ if (carried === 'no_card')
1564
+ return undefined;
1565
+ return carried === 'card' ? cybersourceSecureAcceptanceUnsupported(ctx) : cybersourceSecureAcceptanceUnreadable(ctx);
1566
+ };
1567
+ async function cybersourceSecureAcceptanceUnsupported(ctx) {
1568
+ const { url, method } = ctx.request;
1569
+ ctx.preparationGate.invalidate('unsupported_checkout');
1570
+ ctx.lifecycle.unsupported(CYBERSOURCE_SECURE_ACCEPTANCE_REASON);
1571
+ ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: eventUrl(ctx.vault, url), method, reason: CYBERSOURCE_SECURE_ACCEPTANCE_REASON } });
1572
+ return cybersourceSecureAcceptanceStop(ctx);
1573
+ }
1574
+ async function cybersourceSecureAcceptanceUnreadable(ctx) {
1575
+ ctx.onEvent?.({ type: 'blocked', detail: 'unreadable_cybersource_secure_acceptance_request' });
1576
+ return cybersourceSecureAcceptanceStop(ctx);
1577
+ }
1578
+ /**
1579
+ * Stop a Secure Acceptance post: a form navigation gets the stopped page, and if the page cannot be
1580
+ * answered, or the request is not a navigation, it fails. Either way the card never leaves.
1581
+ */
1582
+ async function cybersourceSecureAcceptanceStop(ctx) {
1583
+ let navigation = false;
1584
+ try {
1585
+ navigation = ctx.request.navigation === true;
1586
+ }
1587
+ catch {
1588
+ navigation = false;
1589
+ }
1590
+ if (navigation) {
1591
+ try {
1592
+ return await stopNavigationBeforeSend(ctx, CYBERSOURCE_SECURE_ACCEPTANCE_RECIPIENT, 'request_unsupported');
1593
+ }
1594
+ catch { /* the page could not be answered: fail the request below */ }
1595
+ }
1596
+ await ctx.effects.abort().catch(() => { });
1597
+ return { kind: 'abort' };
1598
+ }
1533
1599
  // The terminal arbiter, always last. A request that no gate claimed and that is
1534
1600
  // neither a native Stripe step nor a recognized card request continues, unless
1535
1601
  // it matches a declared payment endpoint we do not support (then it aborts as an
@@ -1563,7 +1629,8 @@ async function unsupportedContinue(ctx) {
1563
1629
  * arbiter runs after all of them and is not part of this list. A gate appended
1564
1630
  * after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
1565
1631
  * pauses each one itself. */
1566
- export const PSP_PRE_CLAIM_GATES = [cardEndpointResendPreClaim, cardEndpointPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, wspayPreClaim, shijiStsPreClaim, ekashuPreClaim, paypalHostedFieldsPreClaim];
1632
+ export const PSP_PRE_CLAIM_GATES = [cardEndpointResendPreClaim, cardEndpointPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, wspayPreClaim, shijiStsPreClaim, ekashuPreClaim, paypalHostedFieldsPreClaim,
1633
+ cybersourceSecureAcceptancePreClaim];
1567
1634
  /** Run each gate in order; the first to return a decision wins. If none does,
1568
1635
  * the terminal arbiter decides between continue, abort (unsupported) and pause.
1569
1636
  * Returns synchronously while every gate defers synchronously (an ordinary card
@@ -2172,6 +2172,33 @@
2172
2172
  "hostname_pattern": "^assets\\.braintreegateway\\.com$",
2173
2173
  "pathname_pattern": "^\\/web\\/[0-9]+\\.[0-9]+\\.[0-9]+-payments-sdk(?:-[a-z]+)?\\/html\\/hosted-fields-(?:input-frame|payments-sdk-tokenization-frame)(?:\\.min)?\\.html$"
2174
2174
  },
2175
+ {
2176
+ "id": "cybersource-secure-acceptance-silent-order-post",
2177
+ "psp": "cybersource_secure_acceptance",
2178
+ "kinds": [
2179
+ "form",
2180
+ "document",
2181
+ "frame",
2182
+ "iframe"
2183
+ ],
2184
+ "origin": "https://secureacceptance.cybersource.com",
2185
+ "path_pattern": "/silent/*",
2186
+ "sources": [
2187
+ {
2188
+ "kind": "official_documentation",
2189
+ "reference": "https://developer.cybersource.com/docs/cybs/en-us/sa/developer/all/sa-checkout/secure-acceptance.html",
2190
+ "reviewed_on": "2026-10-08"
2191
+ },
2192
+ {
2193
+ "kind": "saved_observation",
2194
+ "reference": "arrow-2026-10-08:secure-acceptance-silent-embedded-token-create-hidden-form",
2195
+ "reviewed_on": "2026-10-08"
2196
+ }
2197
+ ],
2198
+ "variant_unverified": "CyberSource Secure Acceptance Silent Order POST sends the card from the merchant page to /silent/ on CyberSource's Secure Acceptance hosts; no Vault mode takes that request over.",
2199
+ "hostname_pattern": "^secureacceptance\\.cybersource\\.com$",
2200
+ "pathname_pattern": "^\\/silent\\/(?:[A-Za-z0-9_-]+\\/)*[A-Za-z0-9_-]+\\/?$"
2201
+ },
2175
2202
  {
2176
2203
  "id": "fiserv-commercehub-payment-fields",
2177
2204
  "psp": "fiserv",
@@ -3432,6 +3459,60 @@
3432
3459
  "hostname_pattern": "^testflex\\.cybersource\\.com$",
3433
3460
  "pathname_pattern": "^\\/microform\\/bundle\\/v2\\.[0-9]+\\.[0-9]+\\/iframe\\.html$"
3434
3461
  },
3462
+ {
3463
+ "id": "cybersource-secure-acceptance-silent-order-post-test",
3464
+ "psp": "cybersource_secure_acceptance",
3465
+ "kinds": [
3466
+ "form",
3467
+ "document",
3468
+ "frame",
3469
+ "iframe"
3470
+ ],
3471
+ "origin": "https://testsecureacceptance.cybersource.com",
3472
+ "path_pattern": "/silent/*",
3473
+ "sources": [
3474
+ {
3475
+ "kind": "official_documentation",
3476
+ "reference": "https://developer.cybersource.com/docs/cybs/en-us/sa/developer/all/sa-checkout/secure-acceptance.html",
3477
+ "reviewed_on": "2026-10-08"
3478
+ },
3479
+ {
3480
+ "kind": "saved_observation",
3481
+ "reference": "arrow-2026-10-08:secure-acceptance-silent-embedded-token-create-hidden-form",
3482
+ "reviewed_on": "2026-10-08"
3483
+ }
3484
+ ],
3485
+ "variant_unverified": "CyberSource Secure Acceptance Silent Order POST sends the card from the merchant page to /silent/ on CyberSource's Secure Acceptance hosts; no Vault mode takes that request over.",
3486
+ "hostname_pattern": "^testsecureacceptance\\.cybersource\\.com$",
3487
+ "pathname_pattern": "^\\/silent\\/(?:[A-Za-z0-9_-]+\\/)*[A-Za-z0-9_-]+\\/?$"
3488
+ },
3489
+ {
3490
+ "id": "cybersource-secure-acceptance-silent-order-post-in",
3491
+ "psp": "cybersource_secure_acceptance",
3492
+ "kinds": [
3493
+ "form",
3494
+ "document",
3495
+ "frame",
3496
+ "iframe"
3497
+ ],
3498
+ "origin": "https://secureacceptance.in.cybersource.com",
3499
+ "path_pattern": "/silent/*",
3500
+ "sources": [
3501
+ {
3502
+ "kind": "official_documentation",
3503
+ "reference": "https://developer.cybersource.com/docs/cybs/en-us/sa/developer/all/sa-checkout/secure-acceptance.html",
3504
+ "reviewed_on": "2026-10-08"
3505
+ },
3506
+ {
3507
+ "kind": "saved_observation",
3508
+ "reference": "arrow-2026-10-08:secure-acceptance-silent-embedded-token-create-hidden-form",
3509
+ "reviewed_on": "2026-10-08"
3510
+ }
3511
+ ],
3512
+ "variant_unverified": "CyberSource Secure Acceptance Silent Order POST sends the card from the merchant page to /silent/ on CyberSource's Secure Acceptance hosts; no Vault mode takes that request over.",
3513
+ "hostname_pattern": "^secureacceptance\\.in\\.cybersource\\.com$",
3514
+ "pathname_pattern": "^\\/silent\\/(?:[A-Za-z0-9_-]+\\/)*[A-Za-z0-9_-]+\\/?$"
3515
+ },
3435
3516
  {
3436
3517
  "id": "worldpay-eprotect-js-api-prelive",
3437
3518
  "psp": "worldpay_eprotect",
@@ -469,6 +469,7 @@
469
469
  "gravy-secure-fields-script",
470
470
  "gravy-secure-fields-script-sandbox",
471
471
  "paypal-hosted-fields-payments-sdk",
472
+ "cybersource-secure-acceptance-silent-order-post",
472
473
  "fiserv-commercehub-payment-fields",
473
474
  "fiserv-commercehub-checkout-script",
474
475
  "fiserv-secure-data-capture-frame",
@@ -523,6 +524,8 @@
523
524
  "gmo-link-hosted-page-test",
524
525
  "cybersource-flex-microform-script-test",
525
526
  "cybersource-flex-microform-field-test",
527
+ "cybersource-secure-acceptance-silent-order-post-test",
528
+ "cybersource-secure-acceptance-silent-order-post-in",
526
529
  "worldpay-eprotect-js-api-prelive",
527
530
  "worldpay-eprotect-iframe-client-prelive",
528
531
  "worldpay-eprotect-payframe-prelive",
@@ -1100,6 +1103,7 @@
1100
1103
  "gravy-secure-fields-script",
1101
1104
  "gravy-secure-fields-script-sandbox",
1102
1105
  "paypal-hosted-fields-payments-sdk",
1106
+ "cybersource-secure-acceptance-silent-order-post",
1103
1107
  "fiserv-commercehub-payment-fields",
1104
1108
  "fiserv-commercehub-checkout-script",
1105
1109
  "fiserv-secure-data-capture-frame",
@@ -1154,6 +1158,8 @@
1154
1158
  "gmo-link-hosted-page-test",
1155
1159
  "cybersource-flex-microform-script-test",
1156
1160
  "cybersource-flex-microform-field-test",
1161
+ "cybersource-secure-acceptance-silent-order-post-test",
1162
+ "cybersource-secure-acceptance-silent-order-post-in",
1157
1163
  "worldpay-eprotect-js-api-prelive",
1158
1164
  "worldpay-eprotect-iframe-client-prelive",
1159
1165
  "worldpay-eprotect-payframe-prelive",
@@ -1,5 +1,5 @@
1
1
  // Generated from @agent-cards/payment-core. Do not edit.
2
- // source-sha256: 9d383b5155c791fc68947784fe3e2c16d03d3056572fb353f98305358fe75bc9
2
+ // source-sha256: fc24e6cb50ab112c3a15983c461185a9189bd0cf8c5e1ceb73e8432e6eebc37f
3
3
  import { CHECKOUT_PREFLIGHT_CAPABILITIES } from './preflight-capabilities.generated.js';
4
4
  /** Advisory page discovery. These rules never admit a payment destination. */
5
5
  export const CHECKOUT_PREFLIGHT_VERSION = '1';
@@ -79,6 +79,23 @@ RULES.push({ id: 'paypal-hosted-fields-payments-sdk', psp: 'paypal', kinds: ['fr
79
79
  paypalHostedFieldsSource('https://www.paypal.com/sdk/js?components=hosted-fields'),
80
80
  { kind: 'saved_observation', reference: 'planet-golf-uk-2026-10-06:paypal-hosted-fields-payments-sdk-frames', reviewed_on: '2026-10-07' }],
81
81
  variant_unverified: 'PayPal legacy Hosted Fields frames send the card to the Orders v2 confirm-payment-source on cors.api.paypal.com; no Vault mode takes that request over.' });
82
+ // CyberSource Secure Acceptance's Silent Order POST (the Secure Acceptance Checkout API), reviewed 2026-10-08 from
83
+ // CyberSource's endpoint table and Arrow Electronics' public bundles (no card was sent). The merchant's own page
84
+ // posts the card fields, unsigned, beside the fields its server signed, to a /silent/ path on CyberSource's Secure
85
+ // Acceptance hosts: production, test (testsecureacceptance) and production in India (secureacceptance.in), below.
86
+ // Arrow submits that form into a hidden iframe; a merchant can also post it as the page's own form. No Vault mode
87
+ // takes that post over, and the checkout SDK stops it (checkout/src/cybersource-secure-acceptance.ts). So the form,
88
+ // and a frame or document on a /silent/ path, name cybersource_secure_acceptance and the result stays unknown
89
+ // through `variant_unverified`. Hosted Checkout, the same hosts without /silent/, takes the card on CyberSource's
90
+ // own page and is not named here.
91
+ const secureAcceptanceSources = [
92
+ { kind: 'official_documentation', reference: 'https://developer.cybersource.com/docs/cybs/en-us/sa/developer/all/sa-checkout/secure-acceptance.html', reviewed_on: '2026-10-08' },
93
+ { kind: 'saved_observation', reference: 'arrow-2026-10-08:secure-acceptance-silent-embedded-token-create-hidden-form', reviewed_on: '2026-10-08' },
94
+ ];
95
+ RULES.push({ id: 'cybersource-secure-acceptance-silent-order-post', psp: 'cybersource_secure_acceptance', kinds: ['form', 'document', 'frame', 'iframe'],
96
+ hostname: /^secureacceptance\.cybersource\.com$/, pathname: /^\/silent\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]+\/?$/,
97
+ origin: 'https://secureacceptance.cybersource.com', path_pattern: '/silent/*', sources: secureAcceptanceSources,
98
+ variant_unverified: 'CyberSource Secure Acceptance Silent Order POST sends the card from the merchant page to /silent/ on CyberSource\'s Secure Acceptance hosts; no Vault mode takes that request over.' });
82
99
  // Fiserv Commerce Hub, read 2026-09-23 from Home Depot's OrangePay frame and Fiserv's
83
100
  // docs: payment-fields.js renders one Secure Data Capture iframe per card field, and
84
101
  // the Checkout JS SDK is Commerce Hub's other browser entry. Fiserv ships dark (its
@@ -310,6 +327,9 @@ for (const [baseId, suffix, hostname, origin] of [
310
327
  ['gmo-link-hosted-page', 'test', /^pt01\.mul-pay\.jp$/, 'https://pt01.mul-pay.jp'],
311
328
  ['cybersource-flex-microform-script', 'test', /^testflex\.cybersource\.com$/, 'https://testflex.cybersource.com'],
312
329
  ['cybersource-flex-microform-field', 'test', /^testflex\.cybersource\.com$/, 'https://testflex.cybersource.com'],
330
+ // Secure Acceptance's test host and its production host in India serve the same /silent/ paths.
331
+ ['cybersource-secure-acceptance-silent-order-post', 'test', /^testsecureacceptance\.cybersource\.com$/, 'https://testsecureacceptance.cybersource.com'],
332
+ ['cybersource-secure-acceptance-silent-order-post', 'in', /^secureacceptance\.in\.cybersource\.com$/, 'https://secureacceptance.in.cybersource.com'],
313
333
  ['worldpay-eprotect-js-api', 'prelive', /^request\.eprotect\.vantivprelive\.com$/, 'https://request.eprotect.vantivprelive.com'],
314
334
  ['worldpay-eprotect-iframe-client', 'prelive', /^request\.eprotect\.vantivprelive\.com$/, 'https://request.eprotect.vantivprelive.com'],
315
335
  ['worldpay-eprotect-payframe', 'prelive', /^request\.eprotect\.vantivprelive\.com$/, 'https://request.eprotect.vantivprelive.com'],