@agent-cards/checkout 0.7.0 → 0.9.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 +15 -1
- package/PREFLIGHT.md +294 -0
- package/README.md +16 -3
- 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 +4 -2
- package/dist/cdp.js +38 -6
- package/dist/client.d.ts +13 -4
- package/dist/client.js +80 -8
- package/dist/index.d.ts +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/playwright.d.ts +3 -0
- package/dist/playwright.js +3 -0
- package/dist/preflight-capabilities.generated.d.ts +1165 -0
- package/dist/preflight-capabilities.generated.js +1799 -0
- package/dist/preflight-catalog.json +3892 -0
- package/dist/preflight-playwright.d.ts +34 -0
- package/dist/preflight-playwright.js +262 -0
- package/dist/preflight-schemas.json +1063 -0
- package/dist/preflight.d.ts +1 -0
- package/dist/preflight.generated.d.ts +1867 -0
- package/dist/preflight.generated.js +482 -0
- package/dist/preflight.js +2 -0
- 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/examples/preflight/classify-direct.mjs +21 -0
- package/examples/preflight/classify-kernel.mjs +30 -0
- package/examples/preflight/inspect-browser.mjs +44 -0
- package/examples/preflight/kernel-profile.empty.json +11 -0
- package/examples/preflight/mollie-hosted.observations.json +23 -0
- package/examples/preflight/mollie-hosted.result.json +103 -0
- package/examples/preflight/stripe-script.direct.result.json +92 -0
- package/examples/preflight/stripe-script.observations.json +16 -0
- package/examples/preflight/stripe-script.result.json +87 -0
- package/package.json +16 -7
package/dist/cdp.js
CHANGED
|
@@ -19,7 +19,7 @@ import { CheckoutLifecycle, paymentEndpointGuards } from './lifecycle.js';
|
|
|
19
19
|
* (hostedFormSubmittedPage), and the merchant page may still carry the same
|
|
20
20
|
* card form: an agent that clicks Pay again would pause an identical
|
|
21
21
|
* navigation and, without this, raise a second prompt on the household's
|
|
22
|
-
*
|
|
22
|
+
* device for a payment that already left it. The API's duplicate guard covers
|
|
23
23
|
* a body the form regenerated (same supplier, session and amount inside 15
|
|
24
24
|
* minutes: 409 duplicate_submission, which the adapters quiet the way they
|
|
25
25
|
* quiet a decline, see isApprovalOutcome); this latch covers the
|
|
@@ -38,7 +38,7 @@ const HOSTED_FORM_REPEAT_REASON = 'already submitted on the cardholder\'s device
|
|
|
38
38
|
* This exists because the two failure shapes look identical at the route
|
|
39
39
|
* handler and need opposite answers. A merchant page re-issues an aborted
|
|
40
40
|
* tokenization within milliseconds, and re-prompting on each of those turns
|
|
41
|
-
* one decline into a queue of notifications on someone's
|
|
41
|
+
* one decline into a queue of notifications on someone's device. A genuinely
|
|
42
42
|
* later checkout also arrives as a paused request, and that one deserves a
|
|
43
43
|
* fresh prompt, so a permanent latch is wrong too.
|
|
44
44
|
*
|
|
@@ -282,9 +282,23 @@ function headerEntries(headers) {
|
|
|
282
282
|
* undefined and the authorization goes out without one (a merchant, category
|
|
283
283
|
* or place rule then refuses it as unknown, never the request itself).
|
|
284
284
|
*/
|
|
285
|
-
async function pageOriginOf(readDocumentUrl) {
|
|
285
|
+
async function pageOriginOf(readDocumentUrl, signal) {
|
|
286
|
+
let timer;
|
|
287
|
+
let abort;
|
|
286
288
|
try {
|
|
287
|
-
const
|
|
289
|
+
const documentUrl = await Promise.race([
|
|
290
|
+
Promise.resolve().then(readDocumentUrl).catch(() => undefined),
|
|
291
|
+
new Promise((resolve) => {
|
|
292
|
+
timer = setTimeout(resolve, 1_000);
|
|
293
|
+
abort = () => resolve(undefined);
|
|
294
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
295
|
+
if (signal.aborted)
|
|
296
|
+
abort();
|
|
297
|
+
}),
|
|
298
|
+
]);
|
|
299
|
+
if (typeof documentUrl !== 'string')
|
|
300
|
+
return undefined;
|
|
301
|
+
const url = new URL(documentUrl);
|
|
288
302
|
if (url.username || url.password)
|
|
289
303
|
return undefined;
|
|
290
304
|
if (url.protocol === 'https:' || (url.protocol === 'http:' && url.hostname === 'localhost'))
|
|
@@ -294,6 +308,12 @@ async function pageOriginOf(readDocumentUrl) {
|
|
|
294
308
|
catch {
|
|
295
309
|
return undefined;
|
|
296
310
|
}
|
|
311
|
+
finally {
|
|
312
|
+
if (timer)
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
if (abort)
|
|
315
|
+
signal.removeEventListener('abort', abort);
|
|
316
|
+
}
|
|
297
317
|
}
|
|
298
318
|
/**
|
|
299
319
|
* The page total, the lowest amount authority, through the integrator's own
|
|
@@ -587,15 +607,21 @@ export async function attachToCdp(cdp, pageSessionId, opts) {
|
|
|
587
607
|
activeRequest = { attempt, networkId, sessionId, frameId };
|
|
588
608
|
if (preparation)
|
|
589
609
|
await preparationGate.assertDocument();
|
|
590
|
-
const pageOrigin = await pageOriginOf(readDocumentUrl);
|
|
610
|
+
const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
|
|
591
611
|
const pageAmount = await pageAmountOf(opts);
|
|
592
612
|
attempt.assertLive();
|
|
613
|
+
const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
|
|
614
|
+
? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
|
|
615
|
+
attempt.assertLive();
|
|
593
616
|
const replay = await opts.vault.authorize({
|
|
594
617
|
user: opts.user,
|
|
595
618
|
merchant: opts.merchant,
|
|
596
619
|
amount: opts.amount,
|
|
597
620
|
currency: opts.currency,
|
|
598
621
|
cardId: preparation?.cardId ?? opts.cardId,
|
|
622
|
+
executionMode: opts.executionMode,
|
|
623
|
+
grantId: opts.grantId,
|
|
624
|
+
merchantOrigin,
|
|
599
625
|
pageOrigin,
|
|
600
626
|
pageAmount,
|
|
601
627
|
preparation,
|
|
@@ -858,15 +884,21 @@ export async function attachToPlaywright(page, opts) {
|
|
|
858
884
|
activeRequest = { request, frames, attempt };
|
|
859
885
|
if (preparation)
|
|
860
886
|
await preparationGate.assertDocument();
|
|
861
|
-
const pageOrigin = await pageOriginOf(readDocumentUrl);
|
|
887
|
+
const pageOrigin = await pageOriginOf(readDocumentUrl, attempt.signal);
|
|
862
888
|
const pageAmount = await pageAmountOf(opts);
|
|
863
889
|
assertRequestLive();
|
|
890
|
+
const merchantOrigin = preparation?.merchantOrigin ?? (opts.executionMode === 'user_approval'
|
|
891
|
+
? opts.merchantOrigin : pageOrigin?.startsWith('https:') ? pageOrigin : opts.merchantOrigin);
|
|
892
|
+
assertRequestLive();
|
|
864
893
|
const replay = await opts.vault.authorize({
|
|
865
894
|
user: opts.user,
|
|
866
895
|
merchant: opts.merchant,
|
|
867
896
|
amount: opts.amount,
|
|
868
897
|
currency: opts.currency,
|
|
869
898
|
cardId: preparation?.cardId ?? opts.cardId,
|
|
899
|
+
executionMode: opts.executionMode,
|
|
900
|
+
grantId: opts.grantId,
|
|
901
|
+
merchantOrigin,
|
|
870
902
|
pageOrigin,
|
|
871
903
|
pageAmount,
|
|
872
904
|
preparation,
|
package/dist/client.d.ts
CHANGED
|
@@ -29,11 +29,18 @@ export declare const SUPPORTED_MODES: readonly CheckoutMode[];
|
|
|
29
29
|
export type AmountAuthority = 'processor' | 'agent' | 'page' | 'none';
|
|
30
30
|
/** An integer in the smallest unit, or a decimal string in normal units with a point; nothing else. */
|
|
31
31
|
export declare function validAmountInput(amount: unknown): amount is number | string;
|
|
32
|
+
/** How an existing authorization is being handled; omitted by older APIs. */
|
|
33
|
+
export interface ExecutionMetadata {
|
|
34
|
+
executionMode?: 'user_approval' | 'autopilot';
|
|
35
|
+
grantId?: string;
|
|
36
|
+
}
|
|
32
37
|
/**
|
|
33
38
|
* The token flow: the cardholder's device called the processor itself, and
|
|
34
39
|
* this is the processor's answer to replay into the paused request.
|
|
35
40
|
*/
|
|
36
|
-
export interface TokenReplay {
|
|
41
|
+
export interface TokenReplay extends ExecutionMetadata {
|
|
42
|
+
/** Present only after validating the explicit owned-shop purchase receipt. */
|
|
43
|
+
shopOrderId?: string;
|
|
37
44
|
/** Absent on older API versions; always 'token' here. */
|
|
38
45
|
mode?: 'token';
|
|
39
46
|
/** The approved authorization (`cauth_…`). */
|
|
@@ -73,7 +80,7 @@ export interface TokenReplay {
|
|
|
73
80
|
* The processor's answer then reaches the page as it normally would; the
|
|
74
81
|
* merchant's order state is where the outcome shows up.
|
|
75
82
|
*/
|
|
76
|
-
export interface CseReplay {
|
|
83
|
+
export interface CseReplay extends ExecutionMetadata {
|
|
77
84
|
mode: 'cse';
|
|
78
85
|
authorizationId: string;
|
|
79
86
|
substitutions: Substitutions;
|
|
@@ -96,7 +103,7 @@ export interface CseReplay {
|
|
|
96
103
|
* processor evidence on this mode and cannot obtain any. Treat it as "the
|
|
97
104
|
* person paid, or tried to, on their own device" and confirm with the merchant.
|
|
98
105
|
*/
|
|
99
|
-
export interface HostedFormReplay {
|
|
106
|
+
export interface HostedFormReplay extends ExecutionMetadata {
|
|
100
107
|
mode: 'hosted_form';
|
|
101
108
|
/** What this resolution is: a device-attested submission, not a processor answer. */
|
|
102
109
|
kind: 'submitted_on_device';
|
|
@@ -152,7 +159,9 @@ export declare class CheckoutPreparationError extends Error {
|
|
|
152
159
|
reason: string;
|
|
153
160
|
constructor(preparationId: string | null, reason: string);
|
|
154
161
|
}
|
|
155
|
-
export interface AuthorizeInput {
|
|
162
|
+
export interface AuthorizeInput extends ExecutionMetadata {
|
|
163
|
+
/** Observed top-level HTTPS merchant origin; a routing hint, never payment authority. */
|
|
164
|
+
merchantOrigin?: string;
|
|
156
165
|
/** Your identifier for the person whose card should pay. */
|
|
157
166
|
user: string;
|
|
158
167
|
/** Shown to the user on the approval screen. Judged by nothing: the merchant comes from the checkout page. */
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BUILTIN_REGISTRY, cardUrlPatterns as deriveCardUrlPatterns, findRecognizer, } from './registry.js';
|
|
2
2
|
import { matchesPreparedRequest, validPreparationEnvironment } from './prepared-processor.js';
|
|
3
|
+
import { hasOwnedShopMarker, parseOwnedShopOrder, parseOwnedShopReceipt } from './owned-shop.generated.js';
|
|
3
4
|
/**
|
|
4
5
|
* The modes this SDK can finish. Asked for on syncRegistry (the API serves
|
|
5
6
|
* only recognizers in these modes, so a request this build cannot complete
|
|
@@ -237,7 +238,7 @@ export class PresetRefusedError extends ApprovalDeclinedError {
|
|
|
237
238
|
}
|
|
238
239
|
/** A decline reason only the company presets stamp (see PresetRefusedError). */
|
|
239
240
|
export const PRESET_REFUSAL_REASONS = new Set([
|
|
240
|
-
'spend_total_exceeded', 'spend_rate_exceeded', 'spend_rate_unknown',
|
|
241
|
+
'spend_total_exceeded', 'spend_rate_exceeded', 'spend_rate_unknown', 'amount_unknown', 'rate_unavailable',
|
|
241
242
|
'category_denied', 'category_unknown', 'merchant_denied', 'merchant_unknown',
|
|
242
243
|
'geo_denied', 'geo_unknown', 'currency_denied', 'currency_unknown',
|
|
243
244
|
'time_window_denied', 'surface_denied', 'surface_unknown',
|
|
@@ -512,6 +513,27 @@ export class VaultClient {
|
|
|
512
513
|
* response to replay into the browser. Your process never sees a card.
|
|
513
514
|
*/
|
|
514
515
|
async authorize(input) {
|
|
516
|
+
const ownedShop = hasOwnedShopMarker(input.request);
|
|
517
|
+
const shopOrder = ownedShop ? parseOwnedShopOrder(input.request) : null;
|
|
518
|
+
if (ownedShop) {
|
|
519
|
+
if (!shopOrder || input.preparation || input.executionMode === 'user_approval' ||
|
|
520
|
+
!input.merchantOrigin || (input.amount != null && input.amount !== shopOrder.amount &&
|
|
521
|
+
!(typeof input.amount === 'string' && Number(input.amount) * 100 === shopOrder.amount)) ||
|
|
522
|
+
(input.currency !== undefined && input.currency.toLowerCase() !== shopOrder.currency))
|
|
523
|
+
throw new Error('The shop order cannot use this checkout configuration.');
|
|
524
|
+
input = { ...input, amount: shopOrder.amount, currency: shopOrder.currency };
|
|
525
|
+
}
|
|
526
|
+
if (input.executionMode !== undefined && !['user_approval', 'autopilot'].includes(input.executionMode))
|
|
527
|
+
throw new Error('Unsupported checkout execution mode.');
|
|
528
|
+
if (input.grantId !== undefined && !/^apg_[A-Za-z0-9_-]{1,128}$/.test(input.grantId))
|
|
529
|
+
throw new Error('Invalid autopilot grant ID.');
|
|
530
|
+
if (input.preparation && (input.executionMode === 'autopilot' || input.grantId))
|
|
531
|
+
throw new Error('A device preparation cannot also select autopilot.');
|
|
532
|
+
if (input.merchantOrigin !== undefined && !input.preparation) {
|
|
533
|
+
const merchantUrl = new URL(input.merchantOrigin);
|
|
534
|
+
if (merchantUrl.protocol !== 'https:' || merchantUrl.origin !== input.merchantOrigin)
|
|
535
|
+
throw new Error('merchantOrigin must be an exact HTTPS origin.');
|
|
536
|
+
}
|
|
515
537
|
const preparation = input.preparation;
|
|
516
538
|
if (preparation) {
|
|
517
539
|
if (!this.preparations.has(preparation) || this.usedPreparations.has(preparation))
|
|
@@ -582,6 +604,9 @@ export class VaultClient {
|
|
|
582
604
|
// the recognizer and refuses a disagreement before a row exists.
|
|
583
605
|
mode,
|
|
584
606
|
...(input.cardId ? { cardId: input.cardId } : {}),
|
|
607
|
+
...(input.executionMode ? { execution_mode: input.executionMode } : {}),
|
|
608
|
+
...(input.grantId ? { grant_id: input.grantId } : {}),
|
|
609
|
+
...(input.merchantOrigin && !preparation ? { merchant_origin: input.merchantOrigin } : {}),
|
|
585
610
|
// A prepared checkout already carries the page origin as merchant_origin.
|
|
586
611
|
...(preparation
|
|
587
612
|
? { preparation_id: preparation.id, checkout_key: preparation.checkoutKey, merchant_origin: preparation.merchantOrigin }
|
|
@@ -625,6 +650,20 @@ export class VaultClient {
|
|
|
625
650
|
if (!created || typeof created.id !== 'string' || !created.id)
|
|
626
651
|
throw new PaymentOutcomeUnknownError(preparation ? await this.retireUncertainPreparation(preparation, input.onAuthorizationCreated) : null, 'authorization_create_malformed');
|
|
627
652
|
const authorizationId = created.id;
|
|
653
|
+
let execution = {};
|
|
654
|
+
let approvalDelivered = false;
|
|
655
|
+
const deliverApproval = (state) => {
|
|
656
|
+
if (ownedShop || preparation || approvalDelivered || execution.executionMode === 'autopilot')
|
|
657
|
+
return;
|
|
658
|
+
const url = state.approvalUrl ?? state.approval_url;
|
|
659
|
+
if (typeof url !== 'string' || !url)
|
|
660
|
+
return;
|
|
661
|
+
approvalDelivered = true;
|
|
662
|
+
try {
|
|
663
|
+
Promise.resolve(input.onApprovalUrl?.(url)).catch(() => { });
|
|
664
|
+
}
|
|
665
|
+
catch { /* observer only */ }
|
|
666
|
+
};
|
|
628
667
|
let failed = false;
|
|
629
668
|
const stopSignal = input.merchantSignal
|
|
630
669
|
? AbortSignal.any([input.merchantSignal, ...(input.signal ? [input.signal] : [])]) : input.signal;
|
|
@@ -635,12 +674,8 @@ export class VaultClient {
|
|
|
635
674
|
catch { /* observer only */ }
|
|
636
675
|
if (input.merchantSignal?.aborted)
|
|
637
676
|
throw new PaymentOutcomeUnknownError(authorizationId, 'merchant_request_aborted');
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
Promise.resolve(input.onApprovalUrl?.(created.approvalUrl)).catch(() => { });
|
|
641
|
-
}
|
|
642
|
-
catch { /* approval delivery must not lose an existing authorization */ }
|
|
643
|
-
}
|
|
677
|
+
execution = executionMetadata(created, authorizationId);
|
|
678
|
+
deliverApproval(created);
|
|
644
679
|
while (Date.now() < deadline) {
|
|
645
680
|
if (stopSignal?.aborted)
|
|
646
681
|
throw new PaymentOutcomeUnknownError(authorizationId, input.merchantSignal?.aborted ? 'merchant_request_aborted' : 'local_cancel');
|
|
@@ -661,6 +696,11 @@ export class VaultClient {
|
|
|
661
696
|
if (!s || typeof s !== 'object' || Array.isArray(s) || typeof s.status !== 'string') {
|
|
662
697
|
throw new PaymentOutcomeUnknownError(authorizationId, 'authorization_status_malformed');
|
|
663
698
|
}
|
|
699
|
+
execution = executionMetadata(s, authorizationId, execution);
|
|
700
|
+
if (shopOrder && (s.status === 'submitted_on_device' ||
|
|
701
|
+
(s.status === 'approved' && (s.mode !== 'token' || execution.executionMode !== 'autopilot'))))
|
|
702
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'shop_receipt_unconfirmed');
|
|
703
|
+
deliverApproval(s);
|
|
664
704
|
const amountAuthority = typeof s.amount_authority === 'string' && AMOUNT_AUTHORITIES.includes(s.amount_authority)
|
|
665
705
|
? { amountAuthority: s.amount_authority }
|
|
666
706
|
: {};
|
|
@@ -676,7 +716,9 @@ export class VaultClient {
|
|
|
676
716
|
if (typeof s.submitted_at !== 'string' || !s.submitted_at) {
|
|
677
717
|
throw new PaymentOutcomeUnknownError(authorizationId, 'submission_timestamp_missing');
|
|
678
718
|
}
|
|
679
|
-
|
|
719
|
+
if (execution.executionMode === 'autopilot')
|
|
720
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
|
|
721
|
+
return { mode: 'hosted_form', kind: 'submitted_on_device', outcome: 'unverified', authorizationId, submittedAt: s.submitted_at, ...amountAuthority, ...execution };
|
|
680
722
|
}
|
|
681
723
|
if (s.status === 'approved') {
|
|
682
724
|
const approvedMode = typeof s.mode === 'string' ? s.mode : 'token';
|
|
@@ -687,6 +729,8 @@ export class VaultClient {
|
|
|
687
729
|
throw new PaymentOutcomeUnknownError(authorizationId, 'hosted_form_approval_malformed');
|
|
688
730
|
}
|
|
689
731
|
if (approvedMode === 'cse') {
|
|
732
|
+
if (execution.executionMode === 'autopilot')
|
|
733
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_submission_mode_invalid');
|
|
690
734
|
const sub = s.substitutions;
|
|
691
735
|
const fieldsOk = sub && typeof sub === 'object' && sub.encoding === 'json' && typeof sub.at === 'string' && sub.at
|
|
692
736
|
&& sub.fields && typeof sub.fields === 'object' && !Array.isArray(sub.fields)
|
|
@@ -711,6 +755,7 @@ export class VaultClient {
|
|
|
711
755
|
...(removeRaw ? { remove: [...removeRaw] } : {}),
|
|
712
756
|
},
|
|
713
757
|
...amountAuthority,
|
|
758
|
+
...execution,
|
|
714
759
|
};
|
|
715
760
|
}
|
|
716
761
|
if (approvedMode !== 'token')
|
|
@@ -720,15 +765,29 @@ export class VaultClient {
|
|
|
720
765
|
|| typeof response.body !== 'string' || !response.headers || typeof response.headers !== 'object' || Array.isArray(response.headers)) {
|
|
721
766
|
throw new PaymentOutcomeUnknownError(authorizationId, 'approved_response_malformed');
|
|
722
767
|
}
|
|
768
|
+
if (shopOrder) {
|
|
769
|
+
let receipt;
|
|
770
|
+
try {
|
|
771
|
+
receipt = parseOwnedShopReceipt(JSON.parse(response.body), shopOrder);
|
|
772
|
+
}
|
|
773
|
+
catch { /* No raw error or token replay. */ }
|
|
774
|
+
if (execution.executionMode !== 'autopilot' || response.status !== 200 || !receipt)
|
|
775
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'shop_receipt_unconfirmed');
|
|
776
|
+
// Only canonical receipt fields and our content type enter the page.
|
|
777
|
+
response.body = JSON.stringify(receipt);
|
|
778
|
+
response.headers = { 'content-type': 'application/json' };
|
|
779
|
+
}
|
|
723
780
|
return {
|
|
724
781
|
mode: 'token',
|
|
725
782
|
authorizationId,
|
|
783
|
+
...(shopOrder ? { shopOrderId: shopOrder.order_id } : {}),
|
|
726
784
|
...response,
|
|
727
785
|
amountVerified: typeof s.amount_verified === 'boolean' ? s.amount_verified : null,
|
|
728
786
|
chargedAmount: typeof s.charged_amount === 'number' ? s.charged_amount : null,
|
|
729
787
|
chargedCurrency: typeof s.charged_currency === 'string' ? s.charged_currency : null,
|
|
730
788
|
chargedKind: s.charged_kind === 'captured' || s.charged_kind === 'authorized' || s.charged_kind === 'none' ? s.charged_kind : null,
|
|
731
789
|
...amountAuthority,
|
|
790
|
+
...execution,
|
|
732
791
|
};
|
|
733
792
|
}
|
|
734
793
|
if (s.status === 'declined') {
|
|
@@ -916,6 +975,19 @@ export class VaultClient {
|
|
|
916
975
|
return this.call(path, { signal });
|
|
917
976
|
}
|
|
918
977
|
}
|
|
978
|
+
function executionMetadata(state, authorizationId, previous = {}) {
|
|
979
|
+
if (state.execution_mode === undefined)
|
|
980
|
+
return previous;
|
|
981
|
+
if (state.execution_mode !== 'user_approval' && state.execution_mode !== 'autopilot')
|
|
982
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'execution_mode_unrecognized');
|
|
983
|
+
if (state.execution_mode === 'user_approval')
|
|
984
|
+
return { executionMode: 'user_approval' };
|
|
985
|
+
if (typeof state.grant_id !== 'string' || !/^apg_[A-Za-z0-9_-]{1,128}$/.test(state.grant_id))
|
|
986
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_grant_unconfirmed');
|
|
987
|
+
if (previous.grantId && previous.grantId !== state.grant_id)
|
|
988
|
+
throw new PaymentOutcomeUnknownError(authorizationId, 'autopilot_grant_changed');
|
|
989
|
+
return { executionMode: 'autopilot', grantId: state.grant_id };
|
|
990
|
+
}
|
|
919
991
|
/**
|
|
920
992
|
* Forward only the headers the merchant needs to accept the replay. Everything
|
|
921
993
|
* else (cookies, UA, tracing) is dropped so we transmit as little as possible.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { VaultClient, CardEncryptedError, UnsupportedModeError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, redactUrl, SUPPORTED_MODES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, } from './client.js';
|
|
2
|
-
export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, } from './client.js';
|
|
2
|
+
export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, HostedFormReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, ExecutionMetadata, } from './client.js';
|
|
3
3
|
export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
|
|
4
4
|
export { CheckoutAttachmentError } from './attachment.js';
|
|
5
5
|
export type { CdpLike, AttachOptions, CorsOutcome } from './cdp.js';
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -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 };
|