@agent-cards/sdk 0.22.0 → 0.23.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 +46 -8
- package/PREFLIGHT.md +3 -1
- package/README.md +120 -15
- package/dist/adyen-merchant-hosted.generated.js +20 -8
- package/dist/card-endpoint-facts.d.ts +96 -0
- package/dist/card-endpoint-facts.js +275 -0
- package/dist/card-endpoint-page.d.ts +54 -0
- package/dist/card-endpoint-page.js +69 -0
- package/dist/card-endpoint.d.ts +132 -11
- package/dist/card-endpoint.js +169 -17
- package/dist/cdp.js +336 -28
- package/dist/client.d.ts +61 -9
- package/dist/client.js +133 -16
- package/dist/creditguard.generated.d.ts +1 -1
- package/dist/cybersource-flex.d.ts +30 -0
- package/dist/cybersource-flex.generated.d.ts +27 -1
- package/dist/cybersource-flex.generated.js +42 -2
- package/dist/cybersource-flex.js +46 -0
- package/dist/ekashu.d.ts +35 -0
- package/dist/ekashu.js +55 -0
- package/dist/lifecycle.d.ts +7 -4
- package/dist/lifecycle.js +6 -3
- package/dist/merchant-card-endpoints.generated.d.ts +60 -1
- package/dist/merchant-card-endpoints.generated.js +3253 -95
- package/dist/pre-claim.d.ts +9 -0
- package/dist/pre-claim.js +179 -13
- package/dist/preflight-catalog.json +48 -0
- package/dist/preflight-schemas.json +4 -0
- package/dist/preflight.generated.js +11 -1
- package/dist/preparation.d.ts +7 -1
- package/dist/preparation.js +27 -2
- package/dist/stripe-checkout.generated.js +66 -1
- package/dist/verifone.generated.js +20 -8
- package/examples/preflight/kernel-native/inventory.json +1 -1
- package/package.json +2 -2
package/dist/pre-claim.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export type PreClaimDecision = {
|
|
|
42
42
|
cybersourceFlex?: CybersourceFlexPause;
|
|
43
43
|
cardEndpoint?: CardEndpointProfile;
|
|
44
44
|
payrails?: PayrailsSidecar;
|
|
45
|
+
navigation?: 'top' | 'frame';
|
|
45
46
|
};
|
|
46
47
|
/** The adapter mechanics a gate performs. Each is awaited where the inline code
|
|
47
48
|
* awaited it; none swallow their own transport failure (a gate adds a .catch
|
|
@@ -78,6 +79,13 @@ export interface PreClaimContext {
|
|
|
78
79
|
/** The request is a frame's navigation (CDP resourceType Document, Playwright isNavigationRequest). Only the ACI gate reads it. */
|
|
79
80
|
navigation?: boolean;
|
|
80
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* What the browser said the paused request is: a navigation of the page ('top'), of an
|
|
84
|
+
* iframe ('frame'), or neither (null: a fetch, an XHR, a subresource). Read only for a
|
|
85
|
+
* card endpoint whose page submits its form as a navigation; an adapter that cannot
|
|
86
|
+
* tell answers null, which refuses such a card post.
|
|
87
|
+
*/
|
|
88
|
+
navigation?: () => Promise<'top' | 'frame' | null>;
|
|
81
89
|
/** cardOnFilePolicy: the policy the API served with the request's recognizer (VaultClient); absent reads as none.
|
|
82
90
|
* withoutCard is VaultClient's own (card-fields.js requestWithoutCard); optional so a test double may omit it. */
|
|
83
91
|
vault: {
|
|
@@ -191,6 +199,7 @@ export type PreClaimGate = (ctx: PreClaimContext) => PreClaimDecision | undefine
|
|
|
191
199
|
export declare const MERCHANT_BODY_UNREADABLE = "the paused request carries no body the SDK could read, so Agentcard cannot tell whether it carries a card";
|
|
192
200
|
export declare const verifonePreClaim: PreClaimGate;
|
|
193
201
|
export declare const fiservPreClaim: PreClaimGate;
|
|
202
|
+
export declare const ekashuPreClaim: PreClaimGate;
|
|
194
203
|
/** The processor gates, in order. A new family adds one entry here; the terminal
|
|
195
204
|
* arbiter runs after all of them and is not part of this list. A gate appended
|
|
196
205
|
* after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
|
package/dist/pre-claim.js
CHANGED
|
@@ -68,9 +68,11 @@ import { isShijiStsHost, shijiStsVerdict } from './shiji-sts.js';
|
|
|
68
68
|
import { classifyProfileRequest, merchantBodyKeyPathSha256, profileTemplatedUrl } from './merchant-hosted.js';
|
|
69
69
|
import { isFiservCaptureHost, isFiservPausableBody } from './fiserv.js';
|
|
70
70
|
import { CYBERSOURCE_FLEX_PSP, isCybersourceFlexHost } from './cybersource-flex.js';
|
|
71
|
-
import { cardEndpointDetail, cardEndpointFor, cardEndpointLive } from './card-endpoint.js';
|
|
71
|
+
import { cardEndpointCarriesCard, cardEndpointDetail, cardEndpointFor, cardEndpointLive, cardEndpointPasses, cardEndpointResendFor, cardEndpointReviewed, } from './card-endpoint.js';
|
|
72
|
+
import { cardEndpointStoppedPage, refusedNavigationOutcome } from './card-endpoint-page.js';
|
|
72
73
|
import { PAYRAILS_HOLDING_REASONS, PAYRAILS_SESSION_MESSAGE, isPayrailsHost, isPayrailsStaticHost, payrailsKeyRefusal, payrailsRequestKind, payrailsSaveRefusal, } from './payrails.js';
|
|
73
74
|
import { classifyVerifoneProfileRequest, verifoneTemplatedUrl } from './verifone.js';
|
|
75
|
+
import { ekashuEntryUrl, isEkashuEntry, isEkashuHost } from './ekashu.js';
|
|
74
76
|
/**
|
|
75
77
|
* The URL an event may name for a request. For an armed merchant profile's
|
|
76
78
|
* endpoint or a sibling of it: the endpoint origin, the path TEMPLATE and the
|
|
@@ -98,25 +100,158 @@ export function eventUrl(vault, url) {
|
|
|
98
100
|
}
|
|
99
101
|
// A reviewed merchant's own card endpoint (card-endpoint.ts). It runs first: its URLs
|
|
100
102
|
// are merchant origins no processor gate claims, and one never reaches another gate. A
|
|
101
|
-
// POST
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
103
|
+
// POST to exactly a profile's card request URL is held only when it carries a card
|
|
104
|
+
// (cardEndpointCarriesCard: anything at the profile's card number field, or a Luhn-valid
|
|
105
|
+
// number anywhere in the body). Several of these URLs also take requests with no card,
|
|
106
|
+
// such as an IHG stay paid with points, a LookFantastic order whose card a processor's
|
|
107
|
+
// own fields tokenized, a saved card or a SynXis room guaranteed with a token: those
|
|
108
|
+
// defer, and continue untouched as before any profile existed. A body the profile reads as
|
|
109
|
+
// a card-free payment (its payment_method names another method: FastSpring's PayPal and
|
|
110
|
+
// wallets post the same URL) also continues untouched, in every status, with an event that
|
|
111
|
+
// says so, since no approval could cover it; one that names another method but carries a
|
|
112
|
+
// card is held like any card request. A body the adapter could
|
|
113
|
+
// not read, or an empty one, counts as carrying a card. A card request pauses for the claim flow,
|
|
114
|
+
// carrying the profile, when this build's copy of the profile takes payments. In any
|
|
115
|
+
// other status (observe, a fact only a HAR can pin, no reviewed HAR, a recurring
|
|
116
|
+
// charge) it aborts as an unsupported checkout, synchronously and before any create, so
|
|
117
|
+
// the placeholder card never reaches the merchant. The event names the profile only
|
|
118
|
+
// when the body is the request the profile reviewed (its body rules, which never read
|
|
119
|
+
// the status); a card in any other body is refused as an unrecognized payment endpoint
|
|
120
|
+
// naming the endpoint origin alone. No event names the URL's path or query, which carry
|
|
121
|
+
// the merchant's cart and booking ids. Anything else, a GET or a CORS preflight to the
|
|
122
|
+
// same URL included, defers. A profile whose page submits its form as a navigation is
|
|
123
|
+
// judged by cardEndpointNavigationPreClaim instead (below).
|
|
108
124
|
const cardEndpointPreClaim = (ctx) => {
|
|
109
125
|
const profile = cardEndpointFor(ctx.request.url, ctx.request.method);
|
|
110
126
|
if (!profile)
|
|
111
127
|
return undefined;
|
|
128
|
+
if (profile.navigation !== null)
|
|
129
|
+
return cardEndpointNavigationPreClaim(ctx, profile);
|
|
130
|
+
const { body, contentType } = cardEndpointPausedBody(ctx);
|
|
131
|
+
if (!cardEndpointCarriesCard(profile, body, contentType)) {
|
|
132
|
+
// A card-free payment the profile names (payment_method) continues untouched, and says so.
|
|
133
|
+
return profile.payment_method && cardEndpointPasses(profile, body, contentType) ? cardEndpointContinue(ctx, profile) : undefined;
|
|
134
|
+
}
|
|
112
135
|
if (cardEndpointLive(profile))
|
|
113
136
|
return { kind: 'pause', stripeStep: null, cardEndpoint: profile };
|
|
114
|
-
return cardEndpointUnsupported(ctx, profile);
|
|
137
|
+
return cardEndpointUnsupported(ctx, profile, cardEndpointReviewed(profile, body, contentType));
|
|
115
138
|
};
|
|
116
|
-
|
|
139
|
+
/** The paused body and its content type, as the adapter reports them. A reader that throws reads as no body. */
|
|
140
|
+
function cardEndpointPausedBody(ctx) {
|
|
141
|
+
let body;
|
|
142
|
+
try {
|
|
143
|
+
body = ctx.request.body;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
body = null;
|
|
147
|
+
}
|
|
148
|
+
let contentType;
|
|
149
|
+
try {
|
|
150
|
+
for (const [name, value] of Object.entries(ctx.request.headers ?? {})) {
|
|
151
|
+
if (name.toLowerCase() === 'content-type' && typeof value === 'string') {
|
|
152
|
+
contentType = value;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
contentType = undefined;
|
|
159
|
+
}
|
|
160
|
+
return { body, contentType };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Answer a paused card form navigation with the page that says Agentcard stopped it before
|
|
164
|
+
* its card left. While an approval is outstanding, or once a payment for this checkout may
|
|
165
|
+
* already exist, the page says a payment is in progress instead of that nothing was sent
|
|
166
|
+
* (card-endpoint-page.ts refusedNavigationOutcome), and names that payment's authorization.
|
|
167
|
+
*/
|
|
168
|
+
async function stopNavigationBeforeSend(ctx, profile, reason) {
|
|
169
|
+
const state = ctx.lifecycle.getState();
|
|
170
|
+
const outcome = refusedNavigationOutcome({ awaitingApproval: ctx.awaitingApproval(), status: state.status });
|
|
171
|
+
await ctx.effects.answer(cardEndpointStoppedPage({ outcome, reason, merchant: profile.merchant,
|
|
172
|
+
authorizationId: outcome === 'payment_in_progress' ? state.authorizationId : null }));
|
|
173
|
+
return { kind: 'answer' };
|
|
174
|
+
}
|
|
175
|
+
// A card endpoint whose page submits its form as a navigation (profile.navigation 'top'
|
|
176
|
+
// or 'frame'). The same URL takes the page's other posts: a post that carries no card (a
|
|
177
|
+
// cancel, another payment method) continues untouched, in any status. A card post that is
|
|
178
|
+
// not a document navigation is a script's, not the form the review read: it is aborted,
|
|
179
|
+
// since no page waits on it. A card post that navigates the wrong frame, or reaches a
|
|
180
|
+
// profile this build does not pay, is answered with the page that says the payment was
|
|
181
|
+
// stopped before the card left, never aborted: an aborted page load renders nothing and
|
|
182
|
+
// invites another click on Pay. Only the reviewed navigation to a live profile pauses.
|
|
183
|
+
async function cardEndpointNavigationPreClaim(ctx, profile) {
|
|
184
|
+
let body = null;
|
|
185
|
+
let contentType;
|
|
186
|
+
try {
|
|
187
|
+
body = ctx.request.body;
|
|
188
|
+
contentType = Object.entries(ctx.request.headers ?? {}).find(([name]) => name.toLowerCase() === 'content-type')?.[1];
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
body = null;
|
|
192
|
+
}
|
|
193
|
+
if (!cardEndpointCarriesCard(profile, body, contentType)) {
|
|
194
|
+
await ctx.effects.continueRequest();
|
|
195
|
+
return { kind: 'continue' };
|
|
196
|
+
}
|
|
197
|
+
let navigation = null;
|
|
198
|
+
try {
|
|
199
|
+
navigation = ctx.navigation ? await ctx.navigation() : null;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
navigation = null;
|
|
203
|
+
}
|
|
204
|
+
if (navigation === null) {
|
|
205
|
+
ctx.onEvent?.({ type: 'card_endpoint_refused', detail: { ...cardEndpointDetail(profile), reason: 'request_unsupported', refusal: 'the card post is not a page navigation' } });
|
|
206
|
+
await ctx.effects.abort();
|
|
207
|
+
return { kind: 'abort' };
|
|
208
|
+
}
|
|
209
|
+
if (navigation !== profile.navigation) {
|
|
210
|
+
ctx.onEvent?.({ type: 'card_endpoint_refused', detail: { ...cardEndpointDetail(profile), reason: 'request_unsupported', refusal: `the card post navigates ${navigation === 'top' ? 'the page' : 'an iframe'}` } });
|
|
211
|
+
return stopNavigationBeforeSend(ctx, profile, 'request_unsupported');
|
|
212
|
+
}
|
|
213
|
+
if (!cardEndpointLive(profile)) {
|
|
214
|
+
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
215
|
+
ctx.lifecycle.unsupported('merchant_card_endpoint_not_enabled');
|
|
216
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { ...cardEndpointDetail(profile), reason: 'merchant_card_endpoint_not_enabled' } });
|
|
217
|
+
return stopNavigationBeforeSend(ctx, profile, 'not_enabled');
|
|
218
|
+
}
|
|
219
|
+
return { kind: 'pause', stripeStep: null, cardEndpoint: profile, navigation };
|
|
220
|
+
}
|
|
221
|
+
async function cardEndpointContinue(ctx, profile) {
|
|
222
|
+
ctx.onEvent?.({ type: 'card_endpoint_passed', detail: { ...cardEndpointDetail(profile), reason: 'card_free_payment' } });
|
|
223
|
+
await ctx.effects.continueRequest().catch(() => { });
|
|
224
|
+
return { kind: 'continue' };
|
|
225
|
+
}
|
|
226
|
+
// A request in which a reviewed merchant's page sends the typed card again outside its
|
|
227
|
+
// card request (the profile's resends: FastSpring's EBANX step posts the number, code
|
|
228
|
+
// and expiry it reads back from its form). It is aborted in every status, before any
|
|
229
|
+
// other gate: the card it carries is the agent's placeholder, which the merchant must
|
|
230
|
+
// never take for a payment, and no approval covers a second card request. The Vault's
|
|
231
|
+
// device withholds the answer that starts that step, so this only fires when something
|
|
232
|
+
// else started it.
|
|
233
|
+
const cardEndpointResendPreClaim = (ctx) => {
|
|
234
|
+
const profile = cardEndpointResendFor(ctx.request.url, ctx.request.method);
|
|
235
|
+
if (!profile)
|
|
236
|
+
return undefined;
|
|
237
|
+
return cardEndpointResendRefused(ctx, profile);
|
|
238
|
+
};
|
|
239
|
+
async function cardEndpointResendRefused(ctx, profile) {
|
|
240
|
+
ctx.lifecycle.unsupported('merchant_card_endpoint_resend_refused');
|
|
241
|
+
ctx.onEvent?.({ type: 'card_endpoint_refused', detail: { ...cardEndpointDetail(profile), reason: 'card_resend' } });
|
|
242
|
+
await ctx.effects.abort();
|
|
243
|
+
return { kind: 'abort' };
|
|
244
|
+
}
|
|
245
|
+
async function cardEndpointUnsupported(ctx, profile, reviewed) {
|
|
117
246
|
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
118
|
-
|
|
119
|
-
|
|
247
|
+
if (reviewed) {
|
|
248
|
+
ctx.lifecycle.unsupported('merchant_card_endpoint_not_enabled');
|
|
249
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { ...cardEndpointDetail(profile), reason: 'merchant_card_endpoint_not_enabled' } });
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
ctx.lifecycle.unsupported();
|
|
253
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { endpoint: profile.endpoint, method: ctx.request.method } });
|
|
254
|
+
}
|
|
120
255
|
await ctx.effects.abort();
|
|
121
256
|
return { kind: 'abort' };
|
|
122
257
|
}
|
|
@@ -1311,6 +1446,37 @@ async function shijiStsCompanion(ctx, body) {
|
|
|
1311
1446
|
ctx.onEvent?.({ type: 'companion_continued', detail: { url: redactUrl(url), psp: 'shiji_sts', fields: ['cvv'] } });
|
|
1312
1447
|
return { kind: 'continue' };
|
|
1313
1448
|
}
|
|
1449
|
+
// NMI's hosted payment page (ekashu.ts). Once this build's registry serves NMI's entry (a
|
|
1450
|
+
// capability the API serves only to a company Agentcard turned NMI on for), every request to
|
|
1451
|
+
// NMI's two hosts pauses, and this gate judges the POSTs:
|
|
1452
|
+
// - the merchant's entry (the host's root, no query) defers, and the terminal arbiter pauses it
|
|
1453
|
+
// for the claim flow like any card request: the cardholder's device pays it;
|
|
1454
|
+
// - every other POST to NMI (the card page's own post, its 3-D Secure calls) carries the card
|
|
1455
|
+
// the agent typed, so it aborts as an unsupported checkout, and NMI never sees that card. The
|
|
1456
|
+
// agent's browser only reaches NMI's card page when the entry was not paused (an attachment
|
|
1457
|
+
// made too late, say), and then only the cardholder's device can pay.
|
|
1458
|
+
// A GET (the card page's images, its help page) is not this gate's and continues. With NMI not
|
|
1459
|
+
// served, the entry is no card request, the gate defers, and every request continues as before.
|
|
1460
|
+
// Synchronous until it must abort, like every gate. Exported for the gate registry's tests.
|
|
1461
|
+
export const ekashuPreClaim = (ctx) => {
|
|
1462
|
+
const { url, method } = ctx.request;
|
|
1463
|
+
if (String(method).toUpperCase() !== 'POST' || !isEkashuHost(url))
|
|
1464
|
+
return undefined;
|
|
1465
|
+
const entry = ekashuEntryUrl(url);
|
|
1466
|
+
if (!entry || !ctx.vault.isCardRequest(entry, 'POST'))
|
|
1467
|
+
return undefined;
|
|
1468
|
+
if (isEkashuEntry(url, method))
|
|
1469
|
+
return undefined;
|
|
1470
|
+
return ekashuAbort(ctx);
|
|
1471
|
+
};
|
|
1472
|
+
async function ekashuAbort(ctx) {
|
|
1473
|
+
const { url, method } = ctx.request;
|
|
1474
|
+
ctx.preparationGate.invalidate('unsupported_checkout');
|
|
1475
|
+
ctx.lifecycle.unsupported('processor_card_page');
|
|
1476
|
+
ctx.onEvent?.({ type: 'unsupported_checkout', detail: { url: eventUrl(ctx.vault, url), method, reason: 'processor_card_page' } });
|
|
1477
|
+
await ctx.effects.abort();
|
|
1478
|
+
return { kind: 'abort' };
|
|
1479
|
+
}
|
|
1314
1480
|
// The terminal arbiter, always last. A request that no gate claimed and that is
|
|
1315
1481
|
// neither a native Stripe step nor a recognized card request continues, unless
|
|
1316
1482
|
// it matches a declared payment endpoint we do not support (then it aborts as an
|
|
@@ -1344,7 +1510,7 @@ async function unsupportedContinue(ctx) {
|
|
|
1344
1510
|
* arbiter runs after all of them and is not part of this list. A gate appended
|
|
1345
1511
|
* after Stripe is never offered a step Stripe claimed: Stripe answers, aborts or
|
|
1346
1512
|
* pauses each one itself. */
|
|
1347
|
-
export const PSP_PRE_CLAIM_GATES = [cardEndpointPreClaim, mercadoPreClaim, juspayPreClaim, braintreePreClaim, stripePreClaim, merchantHostedPreClaim, verifonePreClaim, fiservPreClaim, gmoPreClaim, cybersourceFlexPreClaim, datatransPreClaim, pelecardPreClaim, aciPreClaim, creditguardPreClaim, primerPreClaim, payrailsPreClaim, convergePreClaim, eprotectPreClaim, wspayPreClaim, shijiStsPreClaim];
|
|
1513
|
+
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];
|
|
1348
1514
|
/** Run each gate in order; the first to return a decision wins. If none does,
|
|
1349
1515
|
* the terminal arbiter decides between continue, abort (unsupported) and pause.
|
|
1350
1516
|
* Returns synchronously while every gate defers synchronously (an ordinary card
|
|
@@ -2203,6 +2203,30 @@
|
|
|
2203
2203
|
"hostname_pattern": "^commercehub-secure-data-capture\\.fiservapps\\.com$",
|
|
2204
2204
|
"pathname_pattern": "^\\/sdk-secure\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/iframe\\.html$"
|
|
2205
2205
|
},
|
|
2206
|
+
{
|
|
2207
|
+
"id": "ekashu-hosted-payment-page",
|
|
2208
|
+
"psp": "ekashu",
|
|
2209
|
+
"kinds": [
|
|
2210
|
+
"form",
|
|
2211
|
+
"document"
|
|
2212
|
+
],
|
|
2213
|
+
"origin": "https://live.ekashu.com",
|
|
2214
|
+
"path_pattern": "/",
|
|
2215
|
+
"sources": [
|
|
2216
|
+
{
|
|
2217
|
+
"kind": "primary_source",
|
|
2218
|
+
"reference": "https://www.nmi.com/wp-content/uploads/2024/06/Version-AE-eKashu-Hosted-payment-page-Integration-Guide.pdf",
|
|
2219
|
+
"reviewed_on": "2026-09-30"
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
"kind": "primary_source",
|
|
2223
|
+
"reference": "https://web.archive.org/web/20260307103902/https://live.ekashu.com/",
|
|
2224
|
+
"reviewed_on": "2026-09-30"
|
|
2225
|
+
}
|
|
2226
|
+
],
|
|
2227
|
+
"hostname_pattern": "^live\\.ekashu\\.com$",
|
|
2228
|
+
"pathname_pattern": "^\\/(?:index\\.php)?$"
|
|
2229
|
+
},
|
|
2206
2230
|
{
|
|
2207
2231
|
"id": "verifone-secure-card-capture-script",
|
|
2208
2232
|
"psp": "verifone",
|
|
@@ -4257,6 +4281,30 @@
|
|
|
4257
4281
|
"hostname_pattern": "^commercehub-secure-data-capture-cert\\.fiservapps\\.com$",
|
|
4258
4282
|
"pathname_pattern": "^\\/sdk-secure\\/[0-9]+\\.[0-9]+\\.[0-9]+\\/iframe\\.html$"
|
|
4259
4283
|
},
|
|
4284
|
+
{
|
|
4285
|
+
"id": "ekashu-hosted-payment-page-test",
|
|
4286
|
+
"psp": "ekashu",
|
|
4287
|
+
"kinds": [
|
|
4288
|
+
"form",
|
|
4289
|
+
"document"
|
|
4290
|
+
],
|
|
4291
|
+
"origin": "https://test.ekashu.com",
|
|
4292
|
+
"path_pattern": "/",
|
|
4293
|
+
"sources": [
|
|
4294
|
+
{
|
|
4295
|
+
"kind": "primary_source",
|
|
4296
|
+
"reference": "https://www.nmi.com/wp-content/uploads/2024/06/Version-AE-eKashu-Hosted-payment-page-Integration-Guide.pdf",
|
|
4297
|
+
"reviewed_on": "2026-09-30"
|
|
4298
|
+
},
|
|
4299
|
+
{
|
|
4300
|
+
"kind": "primary_source",
|
|
4301
|
+
"reference": "https://web.archive.org/web/20260307103902/https://live.ekashu.com/",
|
|
4302
|
+
"reviewed_on": "2026-09-30"
|
|
4303
|
+
}
|
|
4304
|
+
],
|
|
4305
|
+
"hostname_pattern": "^test\\.ekashu\\.com$",
|
|
4306
|
+
"pathname_pattern": "^\\/(?:index\\.php)?$"
|
|
4307
|
+
},
|
|
4260
4308
|
{
|
|
4261
4309
|
"id": "verifone-secure-card-capture-script-emea",
|
|
4262
4310
|
"psp": "verifone",
|
|
@@ -471,6 +471,7 @@
|
|
|
471
471
|
"fiserv-commercehub-payment-fields",
|
|
472
472
|
"fiserv-commercehub-checkout-script",
|
|
473
473
|
"fiserv-secure-data-capture-frame",
|
|
474
|
+
"ekashu-hosted-payment-page",
|
|
474
475
|
"verifone-secure-card-capture-script",
|
|
475
476
|
"gmo-mp-token-script",
|
|
476
477
|
"gmo-token-v1-script",
|
|
@@ -558,6 +559,7 @@
|
|
|
558
559
|
"fiserv-commercehub-payment-fields-cert",
|
|
559
560
|
"fiserv-commercehub-checkout-script-cert",
|
|
560
561
|
"fiserv-secure-data-capture-frame-cert",
|
|
562
|
+
"ekashu-hosted-payment-page-test",
|
|
561
563
|
"verifone-secure-card-capture-script-emea",
|
|
562
564
|
"verifone-secure-card-capture-script-uscst",
|
|
563
565
|
"verifone-secure-card-capture-script-cst",
|
|
@@ -1099,6 +1101,7 @@
|
|
|
1099
1101
|
"fiserv-commercehub-payment-fields",
|
|
1100
1102
|
"fiserv-commercehub-checkout-script",
|
|
1101
1103
|
"fiserv-secure-data-capture-frame",
|
|
1104
|
+
"ekashu-hosted-payment-page",
|
|
1102
1105
|
"verifone-secure-card-capture-script",
|
|
1103
1106
|
"gmo-mp-token-script",
|
|
1104
1107
|
"gmo-token-v1-script",
|
|
@@ -1186,6 +1189,7 @@
|
|
|
1186
1189
|
"fiserv-commercehub-payment-fields-cert",
|
|
1187
1190
|
"fiserv-commercehub-checkout-script-cert",
|
|
1188
1191
|
"fiserv-secure-data-capture-frame-cert",
|
|
1192
|
+
"ekashu-hosted-payment-page-test",
|
|
1189
1193
|
"verifone-secure-card-capture-script-emea",
|
|
1190
1194
|
"verifone-secure-card-capture-script-uscst",
|
|
1191
1195
|
"verifone-secure-card-capture-script-cst",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
2
|
-
// source-sha256:
|
|
2
|
+
// source-sha256: 3adb75196b940ea807d3290f096e585d6519543d18ec6426daf8b0031acbacb8
|
|
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';
|
|
@@ -68,6 +68,14 @@ RULES.push({ id: 'fiserv-commercehub-payment-fields', psp: 'fiserv', kinds: ['sc
|
|
|
68
68
|
sources: [fiservSource('https://commercehub-checkout.fiservapps.com/sdk/3.8.14/payment-fields.js'), fiservSource('https://orangepay-ecommerce.hdpayments.homedepot.com/env.js')] }, { id: 'fiserv-commercehub-checkout-script', psp: 'fiserv', kinds: ['script'], hostname: /^commercehub-checkout\.fiservapps\.com$/, pathname: /^\/sdk\/[0-9]+\.[0-9]+\.[0-9]+\/checkout\.js$/, origin: 'https://commercehub-checkout.fiservapps.com', path_pattern: '/sdk/:version/checkout.js',
|
|
69
69
|
sources: [fiservSource('https://developer.fiserv.com/product/CommerceHub/docs/Online-Commerce/Integration-Options/Hosted-Checkout/Hosted-Checkout-Version-Release')] }, { id: 'fiserv-secure-data-capture-frame', psp: 'fiserv', kinds: ['frame', 'iframe', 'document'], hostname: /^commercehub-secure-data-capture\.fiservapps\.com$/, pathname: /^\/sdk-secure\/[0-9]+\.[0-9]+\.[0-9]+\/iframe\.html$/, origin: 'https://commercehub-secure-data-capture.fiservapps.com', path_pattern: '/sdk-secure/:version/iframe.html',
|
|
70
70
|
sources: [fiservSource('https://commercehub-checkout.fiservapps.com/sdk/3.8.14/payment-fields.js'), fiservSource('https://commercehub-secure-data-capture.fiservapps.com/sdk-secure/3.8.14/iframe.html')] });
|
|
71
|
+
// NMI Hosted Payment Page (formerly eKashu): the merchant's own entry form posts top-level to
|
|
72
|
+
// live.ekashu.com, and NMI's card page is that host's root document (integration guide AE; the
|
|
73
|
+
// archived card pages in test/fixtures/ekashu). NMI ships dark (its definition names a
|
|
74
|
+
// capability), so the bundled catalog declares no support for it: these identify ekashu and the
|
|
75
|
+
// result stays unknown.
|
|
76
|
+
const ekashuSource = (reference) => ({ kind: 'primary_source', reference, reviewed_on: '2026-09-30' });
|
|
77
|
+
RULES.push({ id: 'ekashu-hosted-payment-page', psp: 'ekashu', kinds: ['form', 'document'], hostname: /^live\.ekashu\.com$/, pathname: /^\/(?:index\.php)?$/, origin: 'https://live.ekashu.com', path_pattern: '/',
|
|
78
|
+
sources: [ekashuSource('https://www.nmi.com/wp-content/uploads/2024/06/Version-AE-eKashu-Hosted-payment-page-Integration-Guide.pdf'), ekashuSource('https://web.archive.org/web/20260307103902/https://live.ekashu.com/')] });
|
|
71
79
|
// Verifone eCommerce, read 2026-09-25 from Verifone's own hosts and docs: a merchant page
|
|
72
80
|
// that encrypts the card itself (Secure Card Capture) loads verifone.js from Verifone's
|
|
73
81
|
// jsclient host for its region, byte-identical on every host. Verifone ships dark (its
|
|
@@ -322,6 +330,8 @@ for (const [baseId, suffix, hostname, origin] of [
|
|
|
322
330
|
['fiserv-commercehub-payment-fields', 'cert', /^commercehub-checkout-cert\.fiservapps\.com$/, 'https://commercehub-checkout-cert.fiservapps.com'],
|
|
323
331
|
['fiserv-commercehub-checkout-script', 'cert', /^commercehub-checkout-cert\.fiservapps\.com$/, 'https://commercehub-checkout-cert.fiservapps.com'],
|
|
324
332
|
['fiserv-secure-data-capture-frame', 'cert', /^commercehub-secure-data-capture-cert\.fiservapps\.com$/, 'https://commercehub-secure-data-capture-cert.fiservapps.com'],
|
|
333
|
+
// NMI's test platform (integration guide AE, "Form creation").
|
|
334
|
+
['ekashu-hosted-payment-page', 'test', /^test\.ekashu\.com$/, 'https://test.ekashu.com'],
|
|
325
335
|
// Verifone's other regions and its two sandboxes serve the same verifone.js.
|
|
326
336
|
['verifone-secure-card-capture-script', 'emea', /^emea\.jsclient\.verifone\.cloud$/, 'https://emea.jsclient.verifone.cloud'],
|
|
327
337
|
['verifone-secure-card-capture-script', 'uscst', /^uscst\.jsclient\.vficloud\.net$/, 'https://uscst.jsclient.vficloud.net'],
|
package/dist/preparation.d.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { type PreparedCheckout } from './client.js';
|
|
2
2
|
import type { AttachOptions } from './cdp.js';
|
|
3
3
|
import type { CheckoutLifecycle } from './lifecycle.js';
|
|
4
|
+
import type { LoadedCardMicroform } from './cybersource-flex.js';
|
|
4
5
|
/** A local, one-use rendezvous. It never starts or retries a merchant request. */
|
|
5
6
|
export declare class PreparationGate {
|
|
6
7
|
private readonly opts;
|
|
7
8
|
private readonly lifecycle;
|
|
8
9
|
private readonly readDocumentUrl;
|
|
10
|
+
private readonly readMicroform?;
|
|
9
11
|
private state;
|
|
10
12
|
private observedRequest;
|
|
11
13
|
private documentUrl;
|
|
12
14
|
private prepared?;
|
|
13
15
|
private stop;
|
|
14
16
|
private expiryTimer?;
|
|
15
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `readMicroform`: the adapter's read of the card Microform the page loaded (cybersource-flex.ts
|
|
19
|
+
* loadedCardMicroform), for prepare({ psp: 'cybersource_flex', tokenizeBeforePay: true }).
|
|
20
|
+
*/
|
|
21
|
+
constructor(opts: AttachOptions, lifecycle: CheckoutLifecycle, readDocumentUrl: () => Promise<string>, readMicroform?: (() => LoadedCardMicroform | Promise<LoadedCardMicroform>) | undefined);
|
|
16
22
|
private prepare;
|
|
17
23
|
/**
|
|
18
24
|
* Called for every recognized card request, before any await or local retry guard. The request's
|
package/dist/preparation.js
CHANGED
|
@@ -6,16 +6,22 @@ export class PreparationGate {
|
|
|
6
6
|
opts;
|
|
7
7
|
lifecycle;
|
|
8
8
|
readDocumentUrl;
|
|
9
|
+
readMicroform;
|
|
9
10
|
state = 'unused';
|
|
10
11
|
observedRequest = false;
|
|
11
12
|
documentUrl = '';
|
|
12
13
|
prepared;
|
|
13
14
|
stop = new AbortController();
|
|
14
15
|
expiryTimer;
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* `readMicroform`: the adapter's read of the card Microform the page loaded (cybersource-flex.ts
|
|
18
|
+
* loadedCardMicroform), for prepare({ psp: 'cybersource_flex', tokenizeBeforePay: true }).
|
|
19
|
+
*/
|
|
20
|
+
constructor(opts, lifecycle, readDocumentUrl, readMicroform) {
|
|
16
21
|
this.opts = opts;
|
|
17
22
|
this.lifecycle = lifecycle;
|
|
18
23
|
this.readDocumentUrl = readDocumentUrl;
|
|
24
|
+
this.readMicroform = readMicroform;
|
|
19
25
|
lifecycle.setPreparationHandler(options => this.prepare(options));
|
|
20
26
|
lifecycle.abort.signal.addEventListener('abort', () => this.invalidate('cancelled'), { once: true });
|
|
21
27
|
}
|
|
@@ -39,6 +45,22 @@ export class PreparationGate {
|
|
|
39
45
|
try {
|
|
40
46
|
if (!options || !validPreparationEnvironment(options.psp, options.environment))
|
|
41
47
|
throw new CheckoutPreparationError(null, 'unsupported_processor');
|
|
48
|
+
// CyberSource Flex, tokenized before Pay: the preparation carries the capture context and the
|
|
49
|
+
// fields of the card Microform this page loaded, so the cardholder's device seals the card and
|
|
50
|
+
// gets CyberSource's token when it approves. A page that races createToken against seconds
|
|
51
|
+
// (Alaska Airlines' flight checkout gives it three) is then answered at once after Pay.
|
|
52
|
+
let cybersourceFlex;
|
|
53
|
+
if (options.tokenizeBeforePay !== undefined && typeof options.tokenizeBeforePay !== 'boolean') {
|
|
54
|
+
throw new CheckoutPreparationError(null, 'tokenize_before_pay_invalid');
|
|
55
|
+
}
|
|
56
|
+
if (options.tokenizeBeforePay) {
|
|
57
|
+
if (options.psp !== 'cybersource_flex')
|
|
58
|
+
throw new CheckoutPreparationError(null, 'unsupported_processor');
|
|
59
|
+
const loaded = (await this.readMicroform?.()) ?? { ok: false, reason: 'microform_not_loaded' };
|
|
60
|
+
if (!loaded.ok)
|
|
61
|
+
throw new CheckoutPreparationError(null, loaded.reason);
|
|
62
|
+
cybersourceFlex = { captureContext: loaded.captureContext, microform: { version: loaded.microform.version, fields: [...loaded.microform.fields] } };
|
|
63
|
+
}
|
|
42
64
|
// A Fiserv checkout's merchantProfile names a key pin, which prepareCheckout checks: its
|
|
43
65
|
// capture endpoint is checked below like any processor-hosted checkout's.
|
|
44
66
|
if (options.psp === 'verifone') {
|
|
@@ -88,6 +110,7 @@ export class PreparationGate {
|
|
|
88
110
|
...options, user: this.opts.user, merchant: this.opts.merchant,
|
|
89
111
|
amount: this.opts.amount, currency: this.opts.currency, cardId: this.opts.cardId,
|
|
90
112
|
merchantOrigin: page.origin, checkoutKey: crypto.randomUUID(), timeoutMs: this.opts.timeoutMs, signal,
|
|
113
|
+
...(cybersourceFlex ? { cybersourceFlex } : {}),
|
|
91
114
|
onPreparationCreated: id => this.lifecycle.preparationCreated(id),
|
|
92
115
|
onApprovalUrl: url => {
|
|
93
116
|
if (!signal.aborted) {
|
|
@@ -101,8 +124,10 @@ export class PreparationGate {
|
|
|
101
124
|
});
|
|
102
125
|
this.prepared = prepared;
|
|
103
126
|
// A Fiserv preparation also names the key pin the cardholder approves (merchantProfile).
|
|
127
|
+
// One that tokenizes before Pay names the capture context its cardholder approved under.
|
|
104
128
|
if (prepared.psp !== options.psp || prepared.environment !== options.environment
|
|
105
|
-
|| prepared.merchantProfile !== options.merchantProfile
|
|
129
|
+
|| prepared.merchantProfile !== options.merchantProfile
|
|
130
|
+
|| (prepared.cybersourceFlex?.captureContext ?? null) !== (cybersourceFlex?.captureContext ?? null))
|
|
106
131
|
throw new CheckoutPreparationError(prepared.id, 'checkout_changed');
|
|
107
132
|
if (signal.aborted || this.state !== 'preparing') {
|
|
108
133
|
void this.cancelThenObserve(prepared.id, 'cancelled');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
-
// artifact-sha256:
|
|
3
|
+
// artifact-sha256: 2a58cd64ab81d468d022c21c6f4d6be6e5be4cd29acd1ad8576b549981433afa
|
|
4
4
|
// src/deep-freeze.js
|
|
5
5
|
function deepFreeze(value) {
|
|
6
6
|
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
@@ -180,6 +180,70 @@ var PROCESSOR_DEFINITIONS = deepFreeze([
|
|
|
180
180
|
callbackUrls: ["success_url_address", "fail_url_address", "notify_url_address"]
|
|
181
181
|
}
|
|
182
182
|
},
|
|
183
|
+
{
|
|
184
|
+
psp: "ekashu",
|
|
185
|
+
// NMI Hosted Payment Page (formerly eKashu, the Creditcall gateway): the card page
|
|
186
|
+
// London Luton's drop-off charge, and APCOA's other airport tenants, send the driver to.
|
|
187
|
+
// The merchant's page posts an ENTRY form top-level to live.ekashu.com/ (test:
|
|
188
|
+
// test.ekashu.com/) with the amount, the result URLs and a SHA256HMAC over them (hash
|
|
189
|
+
// code 2.0.0), and no card. NMI answers with its own card page under an HttpOnly session
|
|
190
|
+
// cookie and an anti-forgery token, so a card form rebuilt away from that session is
|
|
191
|
+
// refused by NMI (processors/ekashu.js has the recorded shapes). The paused request is
|
|
192
|
+
// therefore the entry, a Document navigation that carries no card: the cardholder's
|
|
193
|
+
// device repeats it over the end-to-end TLS relay, fills the card into the page NMI
|
|
194
|
+
// returns to it and posts it back in that same session, and the runtime resolves the
|
|
195
|
+
// paused navigation with the hosted_form notice. Only the entry path is matched: NMI's
|
|
196
|
+
// card post (/index.php) and its 3-D Secure calls (ajax_3d_secure_v2_*.php) carry a card,
|
|
197
|
+
// and the SDK refuses them in the agent's browser, so no dummy card ever reaches NMI.
|
|
198
|
+
// Dark: served only to a client that declares the capability, and gated off for every
|
|
199
|
+
// org the operator has not allowlisted.
|
|
200
|
+
capability: "ekashu_hosted_payment_page",
|
|
201
|
+
match: "(?:live|test)\\.ekashu\\.com/",
|
|
202
|
+
hosts: ["^live\\.ekashu\\.com$", "^test\\.ekashu\\.com$"],
|
|
203
|
+
sandboxHosts: ["^test\\.ekashu\\.com$"],
|
|
204
|
+
// A live row reaches live.ekashu.com only and a test row test.ekashu.com only, and the
|
|
205
|
+
// test host receives only documented test cards (device-replay.js).
|
|
206
|
+
hostEnvironmentPinned: true,
|
|
207
|
+
encoding: "form",
|
|
208
|
+
passthroughHeaders: [],
|
|
209
|
+
mode: "hosted_form",
|
|
210
|
+
hostedForm: {
|
|
211
|
+
// The card goes into the page NMI returns to the device, not into the paused entry,
|
|
212
|
+
// which must carry none (processors/ekashu.js checkEkashuEntry). The device never
|
|
213
|
+
// posts a form of its own: the Vault's form-action policy leaves these hosts out.
|
|
214
|
+
cardIn: "processor_page",
|
|
215
|
+
// The server callback too: the device never returns the buyer's browser to the merchant,
|
|
216
|
+
// so a merchant NMI tells of a payment only through that return never records it.
|
|
217
|
+
required: [
|
|
218
|
+
"ekashu_seller_id",
|
|
219
|
+
"ekashu_seller_key",
|
|
220
|
+
"ekashu_amount",
|
|
221
|
+
"ekashu_currency",
|
|
222
|
+
"ekashu_reference",
|
|
223
|
+
"ekashu_hash_code",
|
|
224
|
+
"ekashu_hash_code_type",
|
|
225
|
+
"ekashu_hash_code_version",
|
|
226
|
+
"ekashu_success_url",
|
|
227
|
+
"ekashu_failure_url",
|
|
228
|
+
"ekashu_callback_success_url"
|
|
229
|
+
],
|
|
230
|
+
card: {
|
|
231
|
+
number: "ekashu_card_number",
|
|
232
|
+
month: "ekashu_expires_end_month",
|
|
233
|
+
year: "ekashu_expires_end_year",
|
|
234
|
+
cvc: "ekashu_verification_value",
|
|
235
|
+
firstName: "ekashu_card_first_name",
|
|
236
|
+
lastName: "ekashu_card_last_name"
|
|
237
|
+
},
|
|
238
|
+
amount: { sum: "ekashu_amount", currency: "ekashu_currency" },
|
|
239
|
+
// The terminal, the merchant's own reference and the amount name one payment; NMI's
|
|
240
|
+
// duplicate check (ekashu_duplicate_check) keys on the same reference.
|
|
241
|
+
dedupe: ["ekashu_seller_id", "ekashu_reference", "ekashu_amount", "ekashu_currency"],
|
|
242
|
+
sale: { key: "ekashu_request_type", values: ["AUTH"] },
|
|
243
|
+
token: "ekashu_card_reference",
|
|
244
|
+
callbackUrls: ["ekashu_success_url", "ekashu_failure_url", "ekashu_return_url"]
|
|
245
|
+
}
|
|
246
|
+
},
|
|
183
247
|
// ── Verified tokenizers (plaintext PAN in the body) ───────────────────────
|
|
184
248
|
// Each entry below was checked against the PSP's own shipped browser SDK or a
|
|
185
249
|
// live probe, then run through resolveRecognizerForUrl with positive, sibling
|
|
@@ -1249,6 +1313,7 @@ var PSP_CLIENT_TABLE = deepFreeze(PROCESSOR_DEFINITIONS.map((definition) => ({
|
|
|
1249
1313
|
...definition.hostedForm.card,
|
|
1250
1314
|
...definition.hostedForm.optional ? { optional: definition.hostedForm.optional } : {}
|
|
1251
1315
|
} } : {},
|
|
1316
|
+
...definition.hostedForm?.cardIn ? { formCardIn: definition.hostedForm.cardIn } : {},
|
|
1252
1317
|
...definition.deviceSteps ? { deviceSteps: Object.fromEntries(Object.entries(definition.deviceSteps).map(([step, source]) => [step, new RegExp(source, "i")])) } : {},
|
|
1253
1318
|
...definition.deviceEncrypt ? { deviceEncrypt: definition.deviceEncrypt } : {}
|
|
1254
1319
|
})));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
// Generated from @agent-cards/payment-core. Do not edit.
|
|
3
|
-
// artifact-sha256:
|
|
3
|
+
// artifact-sha256: cf655438763f54e5d5bfea2555f76941ef80dbfd5aa7bc6342a2a75d1ac9cd39
|
|
4
4
|
// src/deep-freeze.js
|
|
5
5
|
function deepFreeze(value) {
|
|
6
6
|
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
@@ -231,9 +231,14 @@ var PLACEHOLDER_NAME = /^[A-Za-z][A-Za-z0-9_]{0,31}$/;
|
|
|
231
231
|
var LITERAL_SEGMENT = /^[A-Za-z0-9._~@+-]{1,256}$/;
|
|
232
232
|
var QUERY_KEY = /^[A-Za-z0-9._~-]{1,64}$/;
|
|
233
233
|
var QUERY_LITERAL = /^[A-Za-z0-9._~,-]{0,256}$/;
|
|
234
|
+
var QUERY_PREFIX = /^(?:[A-Za-z0-9._~,-]|%[0-9A-F]{2}){1,64}$/;
|
|
234
235
|
var MAX_QUERIES = 8;
|
|
235
236
|
var MAX_PAIRS = 16;
|
|
236
237
|
var placeholderValue = (value) => PLACEHOLDER_VALUE.test(value) && value !== "." && !value.includes("..");
|
|
238
|
+
function queryPlaceholder(value) {
|
|
239
|
+
const mark = value.lastIndexOf(":");
|
|
240
|
+
return mark === -1 ? null : { prefix: value.slice(0, mark), name: value.slice(mark + 1) };
|
|
241
|
+
}
|
|
237
242
|
function pathPlaceholders(template, pathname) {
|
|
238
243
|
if (typeof template !== "string" || typeof pathname !== "string")
|
|
239
244
|
return null;
|
|
@@ -275,10 +280,14 @@ function queryPlaceholders(query, text) {
|
|
|
275
280
|
const written = got[i].slice(mark + 1);
|
|
276
281
|
if (typeof value !== "string")
|
|
277
282
|
return null;
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
const placeholder = queryPlaceholder(value);
|
|
284
|
+
if (placeholder) {
|
|
285
|
+
if (!written.startsWith(placeholder.prefix))
|
|
286
|
+
return null;
|
|
287
|
+
const rest = written.slice(placeholder.prefix.length);
|
|
288
|
+
if (!placeholderValue(rest))
|
|
280
289
|
return null;
|
|
281
|
-
values[
|
|
290
|
+
values[placeholder.name] = rest;
|
|
282
291
|
}
|
|
283
292
|
else if (written !== value) {
|
|
284
293
|
return null;
|
|
@@ -327,9 +336,11 @@ function templatePlaceholders(template) {
|
|
|
327
336
|
const queries = Array.isArray(template?.queries) ? template.queries : [template?.query];
|
|
328
337
|
for (const query of queries) {
|
|
329
338
|
if (query && typeof query === "object") {
|
|
330
|
-
for (const value of Object.values(query))
|
|
331
|
-
|
|
332
|
-
|
|
339
|
+
for (const value of Object.values(query)) {
|
|
340
|
+
const placeholder = typeof value === "string" ? queryPlaceholder(value) : null;
|
|
341
|
+
if (placeholder)
|
|
342
|
+
names.push(placeholder.name);
|
|
343
|
+
}
|
|
333
344
|
}
|
|
334
345
|
}
|
|
335
346
|
return names;
|
|
@@ -364,7 +375,8 @@ function queryTemplateRefusal(query) {
|
|
|
364
375
|
return `the query key "${key}" is not unreserved characters`;
|
|
365
376
|
if (typeof value !== "string")
|
|
366
377
|
return `the query value of ${key} is not text`;
|
|
367
|
-
|
|
378
|
+
const placeholder = queryPlaceholder(value);
|
|
379
|
+
if (placeholder ? !PLACEHOLDER_NAME.test(placeholder.name) || placeholder.prefix !== "" && !QUERY_PREFIX.test(placeholder.prefix) : !QUERY_LITERAL.test(value)) {
|
|
368
380
|
return `the query value of ${key} is neither a literal nor a :name placeholder`;
|
|
369
381
|
}
|
|
370
382
|
}
|