@agent-cards/checkout 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +31 -24
- package/dist/braintree.d.ts +2 -10
- package/dist/braintree.generated.d.ts +10 -0
- package/dist/braintree.generated.js +302 -0
- package/dist/braintree.js +2 -302
- package/dist/builtin-registry.generated.d.ts +2 -0
- package/dist/builtin-registry.generated.js +1 -0
- package/dist/cdp.d.ts +23 -9
- package/dist/cdp.js +99 -6
- package/dist/client.d.ts +124 -27
- package/dist/client.js +211 -27
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lifecycle.d.ts +1 -0
- package/dist/lifecycle.js +27 -1
- package/dist/owned-shop.generated.d.ts +24 -0
- package/dist/owned-shop.generated.js +108 -0
- package/dist/preparation.js +3 -2
- package/dist/registry.d.ts +2 -19
- package/dist/registry.js +3 -188
- package/dist/substitute.d.ts +5 -9
- package/dist/substitute.js +5 -69
- package/dist/substitutions.generated.d.ts +10 -0
- package/dist/substitutions.generated.js +66 -0
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
|
|
1
|
+
export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
|
|
2
2
|
export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
|
|
3
3
|
export { CheckoutAttachmentError } from './attachment.js';
|
|
4
4
|
export { substituteEncryptedFields, SubstitutionError } from './substitute.js';
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export declare class CheckoutLifecycle implements CheckoutController {
|
|
|
59
59
|
private cancelled;
|
|
60
60
|
private merchantAborted;
|
|
61
61
|
private unboundStripeToken;
|
|
62
|
+
private ownedShopOrderId;
|
|
62
63
|
private reconciliation;
|
|
63
64
|
private preparationHandler?;
|
|
64
65
|
private preparationUsed;
|
package/dist/lifecycle.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApprovalDeclinedError, ApprovalTimeoutError, CheckoutCancelledError, CheckoutPreparationError, IntentNotConfirmableError, PaymentOutcomeUnknownError, ProcessorRefusedError } from './client.js';
|
|
2
|
+
import { parseOwnedShopReceipt, OWNED_SHOP_SKU, OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY } from './owned-shop.generated.js';
|
|
2
3
|
/** Shared by the raw CDP and Playwright transports. No browser ownership or payment execution lives here. */
|
|
3
4
|
export class CheckoutLifecycle {
|
|
4
5
|
options;
|
|
@@ -8,6 +9,7 @@ export class CheckoutLifecycle {
|
|
|
8
9
|
cancelled = false;
|
|
9
10
|
merchantAborted = false;
|
|
10
11
|
unboundStripeToken = false;
|
|
12
|
+
ownedShopOrderId = null;
|
|
11
13
|
reconciliation = null;
|
|
12
14
|
preparationHandler;
|
|
13
15
|
preparationUsed = false;
|
|
@@ -82,6 +84,20 @@ export class CheckoutLifecycle {
|
|
|
82
84
|
this.set({ ...this.state, status: 'unsupported', reason: 'unrecognized_payment_endpoint' });
|
|
83
85
|
}
|
|
84
86
|
prepareHandoff(replay, requestUrl) {
|
|
87
|
+
if (replay.mode === 'token' && replay.executionMode === 'autopilot' && replay.shopOrderId) {
|
|
88
|
+
let receipt;
|
|
89
|
+
try {
|
|
90
|
+
receipt = parseOwnedShopReceipt(JSON.parse(replay.body), {
|
|
91
|
+
order_id: replay.shopOrderId, sku: OWNED_SHOP_SKU, amount: OWNED_SHOP_AMOUNT_CENTS, currency: OWNED_SHOP_CURRENCY,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch { /* An unrecognized response remains subject to the token continuation guard. */ }
|
|
95
|
+
if (receipt) {
|
|
96
|
+
this.ownedShopOrderId = receipt.order_id;
|
|
97
|
+
this.held = true;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
85
101
|
// A tokenization approval has no authoritative PaymentIntent or amount
|
|
86
102
|
// binding. Neither the first observed confirm nor the returned opaque
|
|
87
103
|
// token can supply one. Hold even in compatibility mode: this attachment
|
|
@@ -102,6 +118,11 @@ export class CheckoutLifecycle {
|
|
|
102
118
|
handedOff(replay) {
|
|
103
119
|
if (this.merchantAborted)
|
|
104
120
|
return;
|
|
121
|
+
if (this.ownedShopOrderId) {
|
|
122
|
+
this.held = true;
|
|
123
|
+
this.set({ status: 'completed', authorizationId: replay.authorizationId, mode: 'token', orderId: this.ownedShopOrderId });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
105
126
|
const mismatch = (!replay.mode || replay.mode === 'token') && replay.amountVerified === false;
|
|
106
127
|
this.held = !!this.options.requireMerchantResult || replay.mode === 'hosted_form' || mismatch || this.unboundStripeToken;
|
|
107
128
|
this.set({ status: 'awaiting_merchant', authorizationId: replay.authorizationId, mode: replay.mode ?? 'token',
|
|
@@ -178,7 +199,10 @@ export class CheckoutLifecycle {
|
|
|
178
199
|
result = { status: 'unknown' };
|
|
179
200
|
if (result.status === 'completed' && typeof result.orderId === 'string' && result.orderId.length > 0) {
|
|
180
201
|
this.held = true;
|
|
181
|
-
|
|
202
|
+
// Merchant completion supersedes the earlier failure or user-action
|
|
203
|
+
// reason; keep the authorization and preparation context for observers.
|
|
204
|
+
const { reason: _previousReason, ...context } = this.state;
|
|
205
|
+
this.set({ ...context, status: 'completed', orderId: result.orderId });
|
|
182
206
|
}
|
|
183
207
|
else if (result.status === 'failed') {
|
|
184
208
|
// Keep the guard armed until the application deliberately starts another attempt.
|
|
@@ -195,6 +219,8 @@ export class CheckoutLifecycle {
|
|
|
195
219
|
return this.getState();
|
|
196
220
|
}
|
|
197
221
|
retryAfterMerchantFailure(result) {
|
|
222
|
+
if (this.ownedShopOrderId)
|
|
223
|
+
throw new Error('This shop payment already has a receipt; another payment is not permitted.');
|
|
198
224
|
if (this.preparationUsed)
|
|
199
225
|
throw new Error('Prepared checkouts are single use. Reconcile this attempt and create a new attachment.');
|
|
200
226
|
if (this.active || this.reconciliation)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
declare var OWNED_SHOP_OPERATION: string;
|
|
2
|
+
declare var OWNED_SHOP_SKU: string;
|
|
3
|
+
declare var OWNED_SHOP_AMOUNT_CENTS: number;
|
|
4
|
+
declare var OWNED_SHOP_CURRENCY: string;
|
|
5
|
+
declare var OWNED_SHOP_ORDER_FIELD: string;
|
|
6
|
+
declare function isOwnedShopPaymentIntentId(value: any): boolean;
|
|
7
|
+
declare function serializeOwnedShopOrder(value: any): string;
|
|
8
|
+
declare function hasOwnedShopMarker(request: any): any;
|
|
9
|
+
declare function parseOwnedShopOrder(request: any): {
|
|
10
|
+
order_id: any;
|
|
11
|
+
sku: string;
|
|
12
|
+
amount: number;
|
|
13
|
+
currency: string;
|
|
14
|
+
} | null;
|
|
15
|
+
declare function parseOwnedShopReceipt(value: any, expectedOrder: any): {
|
|
16
|
+
object: string;
|
|
17
|
+
order_id: any;
|
|
18
|
+
payment_intent_id: any;
|
|
19
|
+
status: string;
|
|
20
|
+
amount: number;
|
|
21
|
+
currency: string;
|
|
22
|
+
livemode: boolean;
|
|
23
|
+
} | null;
|
|
24
|
+
export { OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY, OWNED_SHOP_OPERATION, OWNED_SHOP_ORDER_FIELD, OWNED_SHOP_SKU, hasOwnedShopMarker, isOwnedShopPaymentIntentId, parseOwnedShopOrder, parseOwnedShopReceipt, serializeOwnedShopOrder };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
+
// artifact-sha256: c8cfb844f3de3f156ee741a0167256d2e764ddfb7d243ce813e011761d3b711a
|
|
4
|
+
// src/owned-shop.js
|
|
5
|
+
var OWNED_SHOP_OPERATION = "stripe.owned_shop_token_to_payment_intent.v1";
|
|
6
|
+
var OWNED_SHOP_SKU = "autopilot-pilot-100";
|
|
7
|
+
var OWNED_SHOP_AMOUNT_CENTS = 100;
|
|
8
|
+
var OWNED_SHOP_CURRENCY = "usd";
|
|
9
|
+
var OWNED_SHOP_ORDER_FIELD = "agentcard_shop_order";
|
|
10
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(?![\s\S])/;
|
|
11
|
+
var INTENT = /^pi_[A-Za-z0-9]{1,200}(?![\s\S])/;
|
|
12
|
+
var refuse = () => {
|
|
13
|
+
throw new Error("stripe_owned_shop_refused");
|
|
14
|
+
};
|
|
15
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
16
|
+
var keys = (value, allowed) => record(value) && Object.keys(value).length === allowed.length && Object.keys(value).every((key) => allowed.includes(key));
|
|
17
|
+
function isOwnedShopPaymentIntentId(value) {
|
|
18
|
+
return typeof value === "string" && INTENT.test(value);
|
|
19
|
+
}
|
|
20
|
+
function order(value) {
|
|
21
|
+
if (!keys(value, ["order_id", "sku", "amount", "currency"]) || typeof value.order_id !== "string" || !UUID.test(value.order_id) || value.sku !== OWNED_SHOP_SKU || value.amount !== OWNED_SHOP_AMOUNT_CENTS || value.currency !== OWNED_SHOP_CURRENCY)
|
|
22
|
+
refuse();
|
|
23
|
+
return {
|
|
24
|
+
order_id: value.order_id,
|
|
25
|
+
sku: OWNED_SHOP_SKU,
|
|
26
|
+
amount: OWNED_SHOP_AMOUNT_CENTS,
|
|
27
|
+
currency: OWNED_SHOP_CURRENCY
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function serializeOwnedShopOrder(value) {
|
|
31
|
+
return JSON.stringify(order(value));
|
|
32
|
+
}
|
|
33
|
+
function hasOwnedShopMarker(request) {
|
|
34
|
+
if (typeof request?.body !== "string")
|
|
35
|
+
return false;
|
|
36
|
+
return request.body.split("&").some((part) => {
|
|
37
|
+
const raw = part.split("=", 1)[0];
|
|
38
|
+
const loose = raw.replace(/\+/g, " ").replace(/%([0-9a-f]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
39
|
+
return loose.toLowerCase().includes(OWNED_SHOP_ORDER_FIELD);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function form(body) {
|
|
43
|
+
if (typeof body !== "string" || !body || body.length > 16384 || /[^\x21-\x7e]/.test(body))
|
|
44
|
+
refuse();
|
|
45
|
+
const chunks = body.split("&");
|
|
46
|
+
if (chunks.length > 16)
|
|
47
|
+
refuse();
|
|
48
|
+
const entries = /* @__PURE__ */ new Map();
|
|
49
|
+
const seen = /* @__PURE__ */ new Set();
|
|
50
|
+
for (const chunk of chunks) {
|
|
51
|
+
const eq = chunk.indexOf("=");
|
|
52
|
+
if (eq < 1)
|
|
53
|
+
refuse();
|
|
54
|
+
let key, value;
|
|
55
|
+
try {
|
|
56
|
+
key = decodeURIComponent(chunk.slice(0, eq).replace(/\+/g, " "));
|
|
57
|
+
value = decodeURIComponent(chunk.slice(eq + 1).replace(/\+/g, " "));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
refuse();
|
|
61
|
+
}
|
|
62
|
+
if (seen.has(key.toLowerCase()) || value.length > 2048 || /[\x00-\x1f\x7f]/.test(key + value))
|
|
63
|
+
refuse();
|
|
64
|
+
seen.add(key.toLowerCase());
|
|
65
|
+
entries.set(key, value);
|
|
66
|
+
}
|
|
67
|
+
return entries;
|
|
68
|
+
}
|
|
69
|
+
function parseOwnedShopOrder(request) {
|
|
70
|
+
if (!hasOwnedShopMarker(request))
|
|
71
|
+
return null;
|
|
72
|
+
const entries = form(request.body);
|
|
73
|
+
if (!entries.has(OWNED_SHOP_ORDER_FIELD) || [...entries.keys()].some((key) => key !== OWNED_SHOP_ORDER_FIELD && key.toLowerCase().includes(OWNED_SHOP_ORDER_FIELD)))
|
|
74
|
+
refuse();
|
|
75
|
+
const encoded = entries.get(OWNED_SHOP_ORDER_FIELD);
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(encoded);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
refuse();
|
|
82
|
+
}
|
|
83
|
+
const result = order(parsed);
|
|
84
|
+
if (serializeOwnedShopOrder(result) !== encoded)
|
|
85
|
+
refuse();
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
function parseOwnedShopReceipt(value, expectedOrder) {
|
|
89
|
+
let boundOrder;
|
|
90
|
+
try {
|
|
91
|
+
boundOrder = order(expectedOrder);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (!keys(value, ["object", "order_id", "payment_intent_id", "status", "amount", "currency", "livemode"]) || value.object !== "agentcard_shop_receipt" || value.order_id !== boundOrder.order_id || !isOwnedShopPaymentIntentId(value.payment_intent_id) || value.status !== "succeeded" || value.amount !== OWNED_SHOP_AMOUNT_CENTS || value.currency !== OWNED_SHOP_CURRENCY || value.livemode !== false)
|
|
97
|
+
return null;
|
|
98
|
+
return {
|
|
99
|
+
object: "agentcard_shop_receipt",
|
|
100
|
+
order_id: boundOrder.order_id,
|
|
101
|
+
payment_intent_id: value.payment_intent_id,
|
|
102
|
+
status: "succeeded",
|
|
103
|
+
amount: OWNED_SHOP_AMOUNT_CENTS,
|
|
104
|
+
currency: OWNED_SHOP_CURRENCY,
|
|
105
|
+
livemode: false
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export { OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY, OWNED_SHOP_OPERATION, OWNED_SHOP_ORDER_FIELD, OWNED_SHOP_SKU, hasOwnedShopMarker, isOwnedShopPaymentIntentId, parseOwnedShopOrder, parseOwnedShopReceipt, serializeOwnedShopOrder };
|
package/dist/preparation.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CheckoutPreparationError } from './client.js';
|
|
2
|
+
import { validAmountInput } from './client.js';
|
|
2
3
|
import { matchesPreparedRequest, validPreparationEnvironment, preparationEndpoint } from './prepared-processor.js';
|
|
3
4
|
/** A local, one-use rendezvous. It never starts or retries a merchant request. */
|
|
4
5
|
export class PreparationGate {
|
|
@@ -39,7 +40,7 @@ export class PreparationGate {
|
|
|
39
40
|
const tokenizer = preparationEndpoint(options.psp, options.environment);
|
|
40
41
|
if (!this.opts.vault.isCardRequest(tokenizer, 'POST'))
|
|
41
42
|
throw new CheckoutPreparationError(null, 'processor_interception_unavailable');
|
|
42
|
-
if (!
|
|
43
|
+
if (!validAmountInput(this.opts.amount) || this.opts.amount === 0 || !/^[a-z]{3}$/i.test(this.opts.currency ?? ''))
|
|
43
44
|
throw new CheckoutPreparationError(null, 'amount_required');
|
|
44
45
|
if (signal.aborted)
|
|
45
46
|
throw new CheckoutPreparationError(null, 'cancelled');
|
|
@@ -49,7 +50,7 @@ export class PreparationGate {
|
|
|
49
50
|
throw new CheckoutPreparationError(null, 'merchant_origin_invalid');
|
|
50
51
|
const prepared = await this.opts.vault.prepareCheckout({
|
|
51
52
|
...options, user: this.opts.user, merchant: this.opts.merchant,
|
|
52
|
-
|
|
53
|
+
amount: this.opts.amount, currency: this.opts.currency, cardId: this.opts.cardId,
|
|
53
54
|
merchantOrigin: page.origin, checkoutKey: crypto.randomUUID(), timeoutMs: this.opts.timeoutMs, signal,
|
|
54
55
|
onPreparationCreated: id => this.lifecycle.preparationCreated(id),
|
|
55
56
|
onApprovalUrl: url => {
|
package/dist/registry.d.ts
CHANGED
|
@@ -50,25 +50,8 @@ export interface Recognizer {
|
|
|
50
50
|
/** How the card reaches the processor; absent means 'token'. Carried verbatim by syncRegistry. */
|
|
51
51
|
mode?: CheckoutMode;
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
* This list is the FALLBACK that decides interception whenever
|
|
57
|
-
* `syncRegistry()` is skipped or its fetch fails — and that failure is silent
|
|
58
|
-
* by design ("never break checkout over a registry fetch"). A PSP missing here
|
|
59
|
-
* is not an error: its tokenization request is simply never paused, the vault
|
|
60
|
-
* is never consulted, and the shopper pays with their OWN card.
|
|
61
|
-
*
|
|
62
|
-
* It shipped with only 5 of the service's 22 recognizers, which meant 17
|
|
63
|
-
* processors degraded to a silent wrong-card charge for any integrator whose
|
|
64
|
-
* sync had not landed. Generated from the backend registry
|
|
65
|
-
* (packages/backend/src/services/checkout-recognizers.ts) so the two cannot
|
|
66
|
-
* drift by transcription; `sdk-registry-drift.test.ts` fails CI if they do.
|
|
67
|
-
*
|
|
68
|
-
* Syncing still matters — it is how a NEW processor arrives without an npm
|
|
69
|
-
* release. This list is the floor, not the ceiling.
|
|
70
|
-
*/
|
|
71
|
-
export declare const BUILTIN_REGISTRY: Recognizer[];
|
|
53
|
+
import { BUILTIN_REGISTRY } from './builtin-registry.generated.js';
|
|
54
|
+
export { BUILTIN_REGISTRY };
|
|
72
55
|
export declare function findRecognizer(url: string, registry?: Recognizer[]): Recognizer | null;
|
|
73
56
|
/**
|
|
74
57
|
* Glob url patterns covering every host in `registry` — what to hand
|
package/dist/registry.js
CHANGED
|
@@ -6,194 +6,9 @@
|
|
|
6
6
|
* site shares another. The list is served from the Agentcard API at runtime so
|
|
7
7
|
* integrators pick up new processors without shipping a release.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* This list is the FALLBACK that decides interception whenever
|
|
13
|
-
* `syncRegistry()` is skipped or its fetch fails — and that failure is silent
|
|
14
|
-
* by design ("never break checkout over a registry fetch"). A PSP missing here
|
|
15
|
-
* is not an error: its tokenization request is simply never paused, the vault
|
|
16
|
-
* is never consulted, and the shopper pays with their OWN card.
|
|
17
|
-
*
|
|
18
|
-
* It shipped with only 5 of the service's 22 recognizers, which meant 17
|
|
19
|
-
* processors degraded to a silent wrong-card charge for any integrator whose
|
|
20
|
-
* sync had not landed. Generated from the backend registry
|
|
21
|
-
* (packages/backend/src/services/checkout-recognizers.ts) so the two cannot
|
|
22
|
-
* drift by transcription; `sdk-registry-drift.test.ts` fails CI if they do.
|
|
23
|
-
*
|
|
24
|
-
* Syncing still matters — it is how a NEW processor arrives without an npm
|
|
25
|
-
* release. This list is the floor, not the ceiling.
|
|
26
|
-
*/
|
|
27
|
-
export const BUILTIN_REGISTRY = [
|
|
28
|
-
{
|
|
29
|
-
psp: 'shopify',
|
|
30
|
-
match: /(checkout\.pci\.shopifyinc\.com|deposit\.[a-z0-9-]+\.shopifycs\.com)\/sessions/i,
|
|
31
|
-
hosts: ["^checkout\\.pci\\.shopifyinc\\.com$", "^deposit\\.[a-z0-9-]+\\.shopifycs\\.com$"],
|
|
32
|
-
encoding: 'json',
|
|
33
|
-
passthroughHeaders: [/^shopify-identification-signature$/i],
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
psp: 'stripe',
|
|
37
|
-
match: /api\.stripe\.com\/v1\/(payment_methods|tokens|setup_intents\/seti_\w+\/confirm|payment_intents\/pi_\w+\/confirm)/i,
|
|
38
|
-
hosts: ["^api\\.stripe\\.com$"],
|
|
39
|
-
encoding: 'form',
|
|
40
|
-
passthroughHeaders: [/^authorization$/i, /^stripe-version$/i, /^x-stripe-client-user-agent$/i],
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
psp: 'braintree',
|
|
44
|
-
match: /payments(\.sandbox)?\.braintree-api\.com\/graphql/i,
|
|
45
|
-
hosts: ["^payments\\.braintree-api\\.com$", "^payments\\.sandbox\\.braintree-api\\.com$"],
|
|
46
|
-
encoding: 'json',
|
|
47
|
-
passthroughHeaders: [/^authorization$/i, /^braintree-version$/i],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
psp: 'checkout_com',
|
|
51
|
-
// Flow's native card controller can use either tokenization host.
|
|
52
|
-
match: /(api|card-acquisition-gateway)(\.sandbox)?\.checkout\.com\/tokens/i,
|
|
53
|
-
hosts: [
|
|
54
|
-
"^api\\.checkout\\.com$", "^api\\.sandbox\\.checkout\\.com$",
|
|
55
|
-
"^card-acquisition-gateway\\.checkout\\.com$", "^card-acquisition-gateway\\.sandbox\\.checkout\\.com$",
|
|
56
|
-
],
|
|
57
|
-
encoding: 'json',
|
|
58
|
-
passthroughHeaders: [/^authorization$/i],
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
psp: 'adyen',
|
|
62
|
-
match: /(checkoutshopper-(test|live(-[a-z]+)?)\.adyen\.com|([a-z0-9-]+\.)*adyenpayments\.com)\/checkoutshopper\/v1\/sessions\/[A-Za-z0-9_-]+\/payments/i,
|
|
63
|
-
hosts: ["^checkoutshopper-(test|live(-[a-z]+)?)\\.adyen\\.com$", "^([a-z0-9-]+\\.)*adyenpayments\\.com$"],
|
|
64
|
-
encoding: 'json',
|
|
65
|
-
passthroughHeaders: [],
|
|
66
|
-
clientSideEncrypted: true,
|
|
67
|
-
mode: 'cse',
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
psp: 'tranzila',
|
|
71
|
-
match: /(direct\.tranzila\.com\/process\/n\/|directng\.tranzila\.com\/process\/?)/i,
|
|
72
|
-
hosts: ["^direct\\.tranzila\\.com$", "^directng\\.tranzila\\.com$"],
|
|
73
|
-
encoding: 'form',
|
|
74
|
-
passthroughHeaders: [],
|
|
75
|
-
mode: 'hosted_form',
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
psp: 'square',
|
|
79
|
-
match: /pci-connect\.squareup(?:sandbox)?\.com\/v2\/card-nonce/i,
|
|
80
|
-
hosts: ["^pci-connect\\.squareup\\.com$", "^pci-connect\\.squareupsandbox\\.com$"],
|
|
81
|
-
encoding: 'json',
|
|
82
|
-
passthroughHeaders: [],
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
psp: 'authorize_net',
|
|
86
|
-
match: /(api2|apitest)\.authorize\.net\/xml\/v1\/request\.api/i,
|
|
87
|
-
hosts: ["^api2\\.authorize\\.net$", "^apitest\\.authorize\\.net$"],
|
|
88
|
-
encoding: 'json',
|
|
89
|
-
passthroughHeaders: [],
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
psp: 'worldpay',
|
|
93
|
-
match: /(try\.)?access\.worldpay\.com\/(sessions\/card|verifiedTokens\/sessions)/i,
|
|
94
|
-
hosts: ["^access\\.worldpay\\.com$", "^try\\.access\\.worldpay\\.com$"],
|
|
95
|
-
encoding: 'json',
|
|
96
|
-
passthroughHeaders: [/^accept$/i],
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
psp: 'nuvei',
|
|
100
|
-
match: /(secure|ppp-test)\.safecharge\.com\/ppp\/api\/v1\/(?:cardTokenization|clientPayment|websdk\/initPaymentWithCardTokenization)\.do/i,
|
|
101
|
-
hosts: ["^secure\\.safecharge\\.com$", "^ppp-test\\.safecharge\\.com$"],
|
|
102
|
-
encoding: 'json',
|
|
103
|
-
passthroughHeaders: [],
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
psp: 'airwallex',
|
|
107
|
-
match: /(checkout(\.sandbox)?\.airwallex\.com|pci-api(\.sandbox)?\.airwallex\.com)\/api\/v1\/pa\/(payment_intents\/[^\/?#]+\/confirm|payment_consents\/[^\/?#]+\/verify|payment_methods\/create)(\?|$)/i,
|
|
108
|
-
hosts: ["^checkout\\.airwallex\\.com$", "^checkout\\.sandbox\\.airwallex\\.com$", "^pci-api\\.airwallex\\.com$", "^pci-api\\.sandbox\\.airwallex\\.com$"],
|
|
109
|
-
encoding: 'json',
|
|
110
|
-
passthroughHeaders: [/^client-secret$/i, /^authorization$/i, /^x-auth-token$/i, /^x-on-behalf-of$/i, /^x-api-version$/i],
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
psp: 'rapyd',
|
|
114
|
-
match: /(api|sandboxapi)\.rapyd\.net\/v1\/(?:hosted\/collect\/card\/[^\/?#]+\/payment_method|checkout\/checkout_[A-Za-z0-9]+)(?:[?#]|$)/i,
|
|
115
|
-
hosts: ["^api\\.rapyd\\.net$", "^sandboxapi\\.rapyd\\.net$"],
|
|
116
|
-
encoding: 'json',
|
|
117
|
-
passthroughHeaders: [],
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
psp: 'dlocal',
|
|
121
|
-
match: /ppmcc(-sandbox)?\.dlocal\.com\/cvault\/credit-card\/temporal/i,
|
|
122
|
-
hosts: ["^ppmcc\\.dlocal\\.com$", "^ppmcc-sandbox\\.dlocal\\.com$"],
|
|
123
|
-
encoding: 'json',
|
|
124
|
-
passthroughHeaders: [/^x-fields-api-key$/i, /^x-uow$/i, /^x-dlocal-infrav2$/i],
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
psp: 'ebanx',
|
|
128
|
-
match: /(?:(?:customer-frontier|customer|api-local-latam|api|sandbox-local-latam|sandbox)\.ebanx\.com|(?:api-diamond|api|sandbox)\.ebanxpay\.com)\/ws\/token/i,
|
|
129
|
-
hosts: ["^customer\\.ebanx\\.com$", "^customer-frontier\\.ebanx\\.com$", "^api\\.ebanx\\.com$", "^sandbox\\.ebanx\\.com$", "^api-local-latam\\.ebanx\\.com$", "^sandbox-local-latam\\.ebanx\\.com$", "^api-diamond\\.ebanxpay\\.com$", "^api\\.ebanxpay\\.com$", "^sandbox\\.ebanxpay\\.com$"],
|
|
130
|
-
encoding: 'json',
|
|
131
|
-
passthroughHeaders: [],
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
psp: 'mercado_pago',
|
|
135
|
-
match: /api\.mercadopago\.com\/v1\/card_tokens(?![\w\/-])/i,
|
|
136
|
-
hosts: ["^api\\.mercadopago\\.com$"],
|
|
137
|
-
encoding: 'json',
|
|
138
|
-
passthroughHeaders: [/^x-product-id$/i],
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
psp: 'payu',
|
|
142
|
-
match: /(secure\.payu\.com|(merch-prod|secure)\.snd\.payu\.com)\/api\/front\/tokens/i,
|
|
143
|
-
hosts: ["^secure\\.payu\\.com$", "^(merch-prod|secure)\\.snd\\.payu\\.com$"],
|
|
144
|
-
encoding: 'json',
|
|
145
|
-
passthroughHeaders: [],
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
psp: 'razorpay',
|
|
149
|
-
match: /api\.razorpay\.com\/v1\/(?:standard_checkout\/)?payments\/create\/(?:ajax|checkout|fees)/i,
|
|
150
|
-
hosts: ["^api\\.razorpay\\.com$"],
|
|
151
|
-
encoding: 'form',
|
|
152
|
-
passthroughHeaders: [/^x-razorpay-sessionid$/i, /^x-customer-access-token$/i],
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
psp: 'mollie',
|
|
156
|
-
match: /api\.cc\.mollie\.com\/v1\/card-tokens/i,
|
|
157
|
-
hosts: ["^api\\.cc\\.mollie\\.com$"],
|
|
158
|
-
encoding: 'json',
|
|
159
|
-
passthroughHeaders: [],
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
psp: 'paysafe',
|
|
163
|
-
match: /(?:api(\.test)?\.paysafe\.com\/(?:paymenthub\/v1\/singleusepaymenthandles|js\/api\/v1\/tokenize)|hosted(\.test)?\.paysafe\.com\/checkout\/api\/v1\/tokenize)(?![\w\/-])/i,
|
|
164
|
-
hosts: ["^api\\.paysafe\\.com$", "^api\\.test\\.paysafe\\.com$", "^hosted\\.paysafe\\.com$", "^hosted\\.test\\.paysafe\\.com$"],
|
|
165
|
-
encoding: 'json',
|
|
166
|
-
passthroughHeaders: [/^authorization$/i, /^x-paysafe-credentials$/i, /^correlationid$/i],
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
psp: 'recurly',
|
|
170
|
-
match: /api(\.eu)?\.recurly\.com\/js\/v1\/token(?![\w-])/i,
|
|
171
|
-
hosts: ["^api\\.recurly\\.com$", "^api\\.eu\\.recurly\\.com$"],
|
|
172
|
-
encoding: 'form',
|
|
173
|
-
passthroughHeaders: [/^recurly-credential-checkout-hostname$/i],
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
psp: 'moneris',
|
|
177
|
-
match: /(www3|esqa|gateway|gatewayqa|gatewayt|gatewaydev)\.moneris\.com\/HPPtoken\/request\.php/i,
|
|
178
|
-
hosts: ["^www3\\.moneris\\.com$", "^esqa\\.moneris\\.com$", "^gateway\\.moneris\\.com$", "^gatewayqa\\.moneris\\.com$", "^gatewayt\\.moneris\\.com$", "^gatewaydev\\.moneris\\.com$"],
|
|
179
|
-
encoding: 'form',
|
|
180
|
-
passthroughHeaders: [],
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
psp: 'bambora',
|
|
184
|
-
match: /(api\.na\.bambora\.com|api\.bam\.shift4api\.net)\/scripts\/tokenization\/tokens/i,
|
|
185
|
-
hosts: ["^api\\.na\\.bambora\\.com$", "^api\\.bam\\.shift4api\\.net$"],
|
|
186
|
-
encoding: 'json',
|
|
187
|
-
passthroughHeaders: [],
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
psp: 'global_payments',
|
|
191
|
-
match: /(?:api\.heartlandportico\.com\/SecureSubmit\.v1\/api\/token|cert\.api2\.heartlandportico\.com\/Hps\.Exchange\.PosGateway\.Hpf\.v1\/api\/token|apis(?:\.sandbox)?(?:\.eu)?\.globalpay\.com\/ucp\/(?:merchants\/[^\/]+\/)?payment-methods)/i,
|
|
192
|
-
hosts: ["^api\\.heartlandportico\\.com$", "^cert\\.api2\\.heartlandportico\\.com$", "^apis\\.globalpay\\.com$", "^apis\\.sandbox\\.globalpay\\.com$", "^apis\\.eu\\.globalpay\\.com$", "^apis\\.sandbox\\.eu\\.globalpay\\.com$"],
|
|
193
|
-
encoding: 'json',
|
|
194
|
-
passthroughHeaders: [/^authorization$/i, /^x-gp-version$/i],
|
|
195
|
-
},
|
|
196
|
-
];
|
|
9
|
+
// Pinned fallback generated from the single shared processor catalog.
|
|
10
|
+
import { BUILTIN_REGISTRY } from './builtin-registry.generated.js';
|
|
11
|
+
export { BUILTIN_REGISTRY };
|
|
197
12
|
export function findRecognizer(url, registry = BUILTIN_REGISTRY) {
|
|
198
13
|
// Match the way the SERVER does, on the PARSED url — never as a substring of
|
|
199
14
|
// the raw string. `match` is unanchored, so a raw-string test classifies any
|
package/dist/substitute.d.ts
CHANGED
|
@@ -29,14 +29,10 @@ export interface Substitutions {
|
|
|
29
29
|
*/
|
|
30
30
|
remove?: string[];
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
export declare class SubstitutionError extends Error {
|
|
32
|
+
declare class SubstitutionErrorShape extends Error {
|
|
34
33
|
constructor(message: string);
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* already a string there.
|
|
41
|
-
*/
|
|
42
|
-
export declare function substituteEncryptedFields(body: string, sub: Substitutions): string;
|
|
35
|
+
export type SubstitutionError = SubstitutionErrorShape;
|
|
36
|
+
export declare const SubstitutionError: typeof SubstitutionErrorShape;
|
|
37
|
+
export declare const substituteEncryptedFields: (body: string, sub: Substitutions) => string;
|
|
38
|
+
export {};
|
package/dist/substitute.js
CHANGED
|
@@ -16,72 +16,8 @@
|
|
|
16
16
|
* and Adyen refuses the mismatch), and a body that does not carry the fields
|
|
17
17
|
* is refused rather than guessed at.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
27
|
-
function isPlainObject(v) {
|
|
28
|
-
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Return the paused JSON body with `sub.fields` written over the same-named
|
|
32
|
-
* string members of the object at `sub.at`. Throws SubstitutionError when the
|
|
33
|
-
* body is not JSON, the path does not lead to an object, or any field is not
|
|
34
|
-
* already a string there.
|
|
35
|
-
*/
|
|
36
|
-
export function substituteEncryptedFields(body, sub) {
|
|
37
|
-
if (!sub || sub.encoding !== 'json') {
|
|
38
|
-
throw new SubstitutionError(`unsupported substitution encoding: ${String(sub?.encoding)}`);
|
|
39
|
-
}
|
|
40
|
-
if (typeof sub.at !== 'string' || !sub.at)
|
|
41
|
-
throw new SubstitutionError('substitutions name no path');
|
|
42
|
-
if (!isPlainObject(sub.fields) || Object.keys(sub.fields).length === 0) {
|
|
43
|
-
throw new SubstitutionError('substitutions carry no fields');
|
|
44
|
-
}
|
|
45
|
-
let parsed;
|
|
46
|
-
try {
|
|
47
|
-
parsed = JSON.parse(body);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
throw new SubstitutionError('the paused body is not JSON');
|
|
51
|
-
}
|
|
52
|
-
if (!isPlainObject(parsed))
|
|
53
|
-
throw new SubstitutionError('the paused body is not a JSON object');
|
|
54
|
-
let node = parsed;
|
|
55
|
-
for (const step of sub.at.split('.')) {
|
|
56
|
-
if (!step || FORBIDDEN_KEYS.has(step))
|
|
57
|
-
throw new SubstitutionError(`refusing substitution path ${sub.at}`);
|
|
58
|
-
const next = node[step];
|
|
59
|
-
if (!isPlainObject(next))
|
|
60
|
-
throw new SubstitutionError(`the paused body has no object at ${sub.at}`);
|
|
61
|
-
node = next;
|
|
62
|
-
}
|
|
63
|
-
for (const [key, value] of Object.entries(sub.fields)) {
|
|
64
|
-
if (FORBIDDEN_KEYS.has(key))
|
|
65
|
-
throw new SubstitutionError(`refusing substitution field ${key}`);
|
|
66
|
-
if (typeof value !== 'string' || value.length === 0)
|
|
67
|
-
throw new SubstitutionError(`substitution for ${key} is not a string`);
|
|
68
|
-
if (typeof node[key] !== 'string')
|
|
69
|
-
throw new SubstitutionError(`the paused body has no string field ${sub.at}.${key}`);
|
|
70
|
-
}
|
|
71
|
-
const remove = sub.remove ?? [];
|
|
72
|
-
if (!Array.isArray(remove))
|
|
73
|
-
throw new SubstitutionError('substitutions.remove is not a list');
|
|
74
|
-
for (const key of remove) {
|
|
75
|
-
if (typeof key !== 'string' || !key || FORBIDDEN_KEYS.has(key))
|
|
76
|
-
throw new SubstitutionError(`refusing removal of ${String(key)}`);
|
|
77
|
-
if (key in sub.fields)
|
|
78
|
-
throw new SubstitutionError(`substitution field ${key} is also listed for removal`);
|
|
79
|
-
}
|
|
80
|
-
// Only past every check, so a refused body is never half-written.
|
|
81
|
-
for (const [key, value] of Object.entries(sub.fields))
|
|
82
|
-
node[key] = value;
|
|
83
|
-
for (const key of remove)
|
|
84
|
-
if (Object.prototype.hasOwnProperty.call(node, key))
|
|
85
|
-
delete node[key];
|
|
86
|
-
return JSON.stringify(parsed);
|
|
87
|
-
}
|
|
19
|
+
// The implementation is bundled from the internal core; the published SDK
|
|
20
|
+
// keeps its existing public types and does not require another package at runtime.
|
|
21
|
+
import { substituteEncryptedFields as sharedSubstitute, SubstitutionError as SharedSubstitutionError, } from './substitutions.generated.js';
|
|
22
|
+
export const SubstitutionError = SharedSubstitutionError;
|
|
23
|
+
export const substituteEncryptedFields = sharedSubstitute;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare var SubstitutionError: {
|
|
2
|
+
new (message: any): {
|
|
3
|
+
name: string;
|
|
4
|
+
message: string;
|
|
5
|
+
stack?: string;
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
declare function substituteEncryptedFields(body: any, sub: any): string;
|
|
10
|
+
export { SubstitutionError, substituteEncryptedFields };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
+
// artifact-sha256: a74db713f1742d9bc5dc8efc78b364d2a69b255ee8da7f98345242f02114e6ce
|
|
4
|
+
// src/substitutions.js
|
|
5
|
+
var SubstitutionError = class extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "SubstitutionError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
12
|
+
function isPlainObject(v) {
|
|
13
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
14
|
+
}
|
|
15
|
+
function substituteEncryptedFields(body, sub) {
|
|
16
|
+
if (!sub || sub.encoding !== "json") {
|
|
17
|
+
throw new SubstitutionError(`unsupported substitution encoding: ${String(sub?.encoding)}`);
|
|
18
|
+
}
|
|
19
|
+
if (typeof sub.at !== "string" || !sub.at)
|
|
20
|
+
throw new SubstitutionError("substitutions name no path");
|
|
21
|
+
if (!isPlainObject(sub.fields) || Object.keys(sub.fields).length === 0) {
|
|
22
|
+
throw new SubstitutionError("substitutions carry no fields");
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(body);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new SubstitutionError("the paused body is not JSON");
|
|
30
|
+
}
|
|
31
|
+
if (!isPlainObject(parsed))
|
|
32
|
+
throw new SubstitutionError("the paused body is not a JSON object");
|
|
33
|
+
let node = parsed;
|
|
34
|
+
for (const step of sub.at.split(".")) {
|
|
35
|
+
if (!step || FORBIDDEN_KEYS.has(step))
|
|
36
|
+
throw new SubstitutionError(`refusing substitution path ${sub.at}`);
|
|
37
|
+
const next = node[step];
|
|
38
|
+
if (!isPlainObject(next))
|
|
39
|
+
throw new SubstitutionError(`the paused body has no object at ${sub.at}`);
|
|
40
|
+
node = next;
|
|
41
|
+
}
|
|
42
|
+
for (const [key, value] of Object.entries(sub.fields)) {
|
|
43
|
+
if (FORBIDDEN_KEYS.has(key))
|
|
44
|
+
throw new SubstitutionError(`refusing substitution field ${key}`);
|
|
45
|
+
if (typeof value !== "string" || value.length === 0)
|
|
46
|
+
throw new SubstitutionError(`substitution for ${key} is not a string`);
|
|
47
|
+
if (typeof node[key] !== "string")
|
|
48
|
+
throw new SubstitutionError(`the paused body has no string field ${sub.at}.${key}`);
|
|
49
|
+
}
|
|
50
|
+
const remove = sub.remove ?? [];
|
|
51
|
+
if (!Array.isArray(remove))
|
|
52
|
+
throw new SubstitutionError("substitutions.remove is not a list");
|
|
53
|
+
for (const key of remove) {
|
|
54
|
+
if (typeof key !== "string" || !key || FORBIDDEN_KEYS.has(key))
|
|
55
|
+
throw new SubstitutionError(`refusing removal of ${String(key)}`);
|
|
56
|
+
if (key in sub.fields)
|
|
57
|
+
throw new SubstitutionError(`substitution field ${key} is also listed for removal`);
|
|
58
|
+
}
|
|
59
|
+
for (const [key, value] of Object.entries(sub.fields))
|
|
60
|
+
node[key] = value;
|
|
61
|
+
for (const key of remove)
|
|
62
|
+
if (Object.prototype.hasOwnProperty.call(node, key))
|
|
63
|
+
delete node[key];
|
|
64
|
+
return JSON.stringify(parsed);
|
|
65
|
+
}
|
|
66
|
+
export { SubstitutionError, substituteEncryptedFields };
|