@agent-cards/sdk 0.23.19 → 0.23.21

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +61 -2
  2. package/PREFLIGHT.md +4 -0
  3. package/README.md +175 -7
  4. package/dist/amadeus.d.ts +4 -2
  5. package/dist/amadeus.js +16 -3
  6. package/dist/buy.d.ts +84 -2
  7. package/dist/buy.js +16 -1
  8. package/dist/card-endpoint-facts.d.ts +2 -1
  9. package/dist/card-endpoint-facts.js +18 -9
  10. package/dist/card-endpoint.d.ts +20 -4
  11. package/dist/card-endpoint.js +38 -4
  12. package/dist/card-fields.generated.js +1444 -51
  13. package/dist/cdp.d.ts +29 -0
  14. package/dist/cdp.js +444 -24
  15. package/dist/client.d.ts +134 -7
  16. package/dist/client.js +350 -25
  17. package/dist/comgate.d.ts +50 -0
  18. package/dist/comgate.js +80 -0
  19. package/dist/cybersource-secure-acceptance.generated.d.ts +1 -1
  20. package/dist/eprotect.generated.d.ts +1 -1
  21. package/dist/gmo.generated.js +7 -3
  22. package/dist/hipay.generated.d.ts +56 -0
  23. package/dist/hipay.generated.js +392 -0
  24. package/dist/hold.d.ts +180 -0
  25. package/dist/hold.js +270 -0
  26. package/dist/hosted-entry.d.ts +4 -0
  27. package/dist/hosted-entry.js +32 -0
  28. package/dist/index.d.ts +4 -1
  29. package/dist/index.js +1 -0
  30. package/dist/lifecycle.d.ts +10 -0
  31. package/dist/lifecycle.js +21 -0
  32. package/dist/merchant-card-endpoints.generated.js +102 -11
  33. package/dist/paddle-tokenex.generated.js +7 -3
  34. package/dist/paddle-vgs.generated.d.ts +3 -2
  35. package/dist/paddle-vgs.generated.js +9 -5
  36. package/dist/payrails.generated.js +7 -3
  37. package/dist/pre-claim.d.ts +20 -0
  38. package/dist/pre-claim.js +202 -22
  39. package/dist/preflight-catalog.json +58 -0
  40. package/dist/preflight-schemas.json +4 -0
  41. package/dist/preflight.generated.js +26 -2
  42. package/dist/prepared-processor.d.ts +6 -2
  43. package/dist/prepared-processor.js +18 -2
  44. package/dist/primer.generated.js +1487 -94
  45. package/dist/przelewy24.d.ts +47 -0
  46. package/dist/przelewy24.js +88 -0
  47. package/dist/quickpay.d.ts +44 -0
  48. package/dist/quickpay.js +65 -0
  49. package/dist/registry.d.ts +6 -5
  50. package/dist/registry.js +27 -7
  51. package/dist/shiji-sts.generated.js +7 -3
  52. package/dist/stripe-checkout.generated.js +744 -13
  53. package/dist/tokenex-iframe.d.ts +93 -0
  54. package/dist/tokenex-iframe.generated.d.ts +81 -0
  55. package/dist/tokenex-iframe.generated.js +403 -0
  56. package/dist/tokenex-iframe.js +195 -0
  57. package/dist/transbank.d.ts +45 -0
  58. package/dist/transbank.js +73 -0
  59. package/dist/twoc2p.d.ts +54 -0
  60. package/dist/twoc2p.generated.d.ts +33 -0
  61. package/dist/twoc2p.generated.js +1149 -0
  62. package/dist/twoc2p.js +85 -0
  63. package/dist/voice.d.ts +16 -13
  64. package/dist/voice.js +13 -10
  65. package/examples/preflight/kernel-native/inventory.json +1 -1
  66. package/package.json +3 -3
package/dist/hold.d.ts ADDED
@@ -0,0 +1,180 @@
1
+ /** One kind of card request the hold catches although no recogniser pays it. */
2
+ export interface HoldOnlyRule {
3
+ /** The processor's name, for the record (`bolt`, `placeholder_card`). */
4
+ psp: string;
5
+ /** The card request's address. */
6
+ match: RegExp;
7
+ /** CDP Fetch url patterns that cover `match`, with an optional resource type. */
8
+ patterns: Array<{
9
+ urlPattern: string;
10
+ resourceType?: string;
11
+ }>;
12
+ /** When set, a request to `match` is a card request only if its body passes. */
13
+ body?: RegExp;
14
+ kind: 'tokenize' | 'store_card_post';
15
+ /** The processor's own wait for this request, in ms: the hold cancels before it. */
16
+ deadlineMs: number;
17
+ /** Methods that carry the card here. Default POST. */
18
+ methods?: string[];
19
+ }
20
+ /**
21
+ * Bolt's embedded card frame (connect.bolt.com credit-card-input): a POST of `{ payload, nonce, public_key }` JSON,
22
+ * the card boxed to Bolt's key, to `<base>/token` or `<base>/token/cvv`, with a 20 s timeout and retries.
23
+ */
24
+ export declare const BOLT_HOLD_RULE: HoldOnlyRule;
25
+ /** Each recogniser's own wait for its card request, where it was measured; any other gets HOLD_DEFAULT_DEADLINE_MS. */
26
+ export declare const HOLD_DEADLINES_MS: Readonly<Record<string, number>>;
27
+ export declare const HOLD_DEFAULT_DEADLINE_MS = 30000;
28
+ /** How long before the processor's own wait the hold gives the request up. */
29
+ export declare const HOLD_DEADLINE_MARGIN_MS = 2000;
30
+ /**
31
+ * Any request whose body carries one of the placeholder card numbers, wherever it goes. It cannot see a card a
32
+ * processor's frame encrypts first (Bolt, Adyen): those need their own rule. Null without a usable number.
33
+ */
34
+ export declare function placeholderHoldRule(numbers: readonly string[], deadlineMs?: number): HoldOnlyRule | null;
35
+ /** The hold-only rule a request is a card request under, or null. */
36
+ export declare function matchHoldOnlyRule(request: {
37
+ url: string;
38
+ method: string;
39
+ body: string | null;
40
+ }, rules: readonly HoldOnlyRule[]): HoldOnlyRule | null;
41
+ export interface HoldOptions {
42
+ /**
43
+ * The placeholder card numbers the vault gave this page. Any request whose body carries one is held too. Matched
44
+ * in this process only, never recorded or sent anywhere.
45
+ */
46
+ placeholderNumbers?: readonly string[];
47
+ /** Replaces every recogniser's deadline (tests). */
48
+ deadlineMs?: number;
49
+ /** How long before the processor's deadline the hold gives the request up. Default HOLD_DEADLINE_MARGIN_MS. */
50
+ deadlineMarginMs?: number;
51
+ /** Called once a card request is held. */
52
+ onHeld?: (held: HeldCardRequest) => void;
53
+ }
54
+ /** What the hold records of a held request: never its body or headers, which may carry the card. */
55
+ export interface HeldCardRequest {
56
+ /** The processor, by the recogniser's name (`braintree`), `bolt`, or `placeholder_card`; null when unnamed. */
57
+ psp: string | null;
58
+ /** Origin and path only, with any credential a processor writes into the path masked (redactUrl). */
59
+ endpoint: string;
60
+ kind: 'tokenize' | 'store_card_post' | 'card_request';
61
+ /** Whether a release can answer it: a recogniser the SDK pays, never Bolt or a store's own card post. */
62
+ replayable: boolean;
63
+ heldAt: number;
64
+ deadlineMs: number;
65
+ }
66
+ export type HoldState = 'watching' | 'held' | 'released' | 'cancelled' | 'expired' | 'lost' | 'closed';
67
+ export interface HoldReleaseInput {
68
+ /** The total the person confirmed, an integer in the currency's smallest unit. The approval is created at it. */
69
+ amount: number;
70
+ currency: string;
71
+ /**
72
+ * The total the page shows now, read by the caller (null when it reads none). Released only when it matches: equal
73
+ * to `amount` by default, or at most `amount` with `upTo` (an approval at a ceiling). Anything else cancels.
74
+ */
75
+ pageTotal: {
76
+ amount: number;
77
+ currency: string;
78
+ } | null;
79
+ /** `amount` is a ceiling the page total may stay under (a store that prices tax after the card). */
80
+ upTo?: boolean;
81
+ /**
82
+ * Sandbox only: a store's own card post held by the placeholder rule carries the documented test number only, and
83
+ * is sent on unchanged so the store can answer it (its Review Order). Refused on a live client.
84
+ */
85
+ continuePlaceholder?: boolean;
86
+ }
87
+ export interface HoldReleaseOutcome {
88
+ /**
89
+ * approved_not_charged: sandbox, the approval was approved and the request never sent on.
90
+ * authorized: live, the device's answer reached the page. continued: a placeholder post was sent on (sandbox).
91
+ */
92
+ outcome: 'approved_not_charged' | 'authorized' | 'continued' | 'declined' | 'expired' | 'failed';
93
+ authorizationId?: string | null;
94
+ detail?: string;
95
+ }
96
+ /** The hold of one attachment: `controller.hold`. */
97
+ export interface CardRequestHold {
98
+ readonly state: HoldState;
99
+ readonly held: HeldCardRequest | null;
100
+ /** Card requests failed without being held: a second one, a retry, or one after a decision. */
101
+ readonly refused: number;
102
+ /** Resolves with the held request once one is held, or null after `timeoutMs` or once the hold is over. */
103
+ whenHeld(timeoutMs?: number): Promise<HeldCardRequest | null>;
104
+ release(input: HoldReleaseInput): Promise<HoldReleaseOutcome>;
105
+ /** Fail the held request in the page: nothing was created or sent. Every later card request fails at once. */
106
+ cancel(reason?: string): Promise<void>;
107
+ }
108
+ /** A hold ended without a release: the paused request fails, and every later card request fails at once. */
109
+ export declare class HoldEndedError extends Error {
110
+ readonly reason: string;
111
+ constructor(reason: string);
112
+ }
113
+ /** A second card request while one is held: this request alone fails, the held one and the checkout are untouched. */
114
+ export declare class HoldRefusedError extends Error {
115
+ constructor();
116
+ }
117
+ /** A sandbox release answered: the approval was created and read, and the request was never sent on. */
118
+ export declare class HoldAnsweredError extends Error {
119
+ readonly outcome: HoldReleaseOutcome;
120
+ constructor(outcome: HoldReleaseOutcome);
121
+ }
122
+ /** What the transport does with a held request once the person answered. */
123
+ export type HoldDecision = {
124
+ kind: 'release';
125
+ amount: number;
126
+ currency: string;
127
+ settle: (outcome: HoldReleaseOutcome) => void;
128
+ } | {
129
+ kind: 'continue';
130
+ settle: (outcome: HoldReleaseOutcome) => void;
131
+ };
132
+ /**
133
+ * The hold's state, shared by the transport and the caller. The transport calls `hold` when a card request pauses
134
+ * and awaits the decision; the caller calls `release` or `cancel`. Not exported from the package: the caller sees
135
+ * the CardRequestHold face.
136
+ */
137
+ export declare class HoldGate implements CardRequestHold {
138
+ private readonly options;
139
+ private readonly sandbox;
140
+ private readonly emit;
141
+ private _state;
142
+ private _held;
143
+ private _refused;
144
+ private timer;
145
+ private waiters;
146
+ private pending;
147
+ readonly rules: HoldOnlyRule[];
148
+ constructor(options: HoldOptions, sandbox: boolean, emit: (type: string, detail?: unknown) => void);
149
+ get state(): HoldState;
150
+ get held(): HeldCardRequest | null;
151
+ get refused(): number;
152
+ /** Fetch patterns the hold-only rules need beside the registry's. */
153
+ patterns(): Array<{
154
+ urlPattern: string;
155
+ resourceType?: string;
156
+ }>;
157
+ deadlineFor(psp: string | null, rule?: HoldOnlyRule): number;
158
+ /** A card request that may not be held now (a second one, or one after a decision): counted, and failed by the caller. */
159
+ refuse(psp: string | null, url: string): boolean;
160
+ /**
161
+ * Hold a card request: resolves with the person's decision, or rejects with HoldEndedError when the hold is
162
+ * cancelled, expires, loses its request or closes. The caller (the transport) answers or fails the request.
163
+ */
164
+ hold(input: {
165
+ psp: string | null;
166
+ url: string;
167
+ kind: HeldCardRequest['kind'];
168
+ replayable: boolean;
169
+ rule?: HoldOnlyRule;
170
+ }): Promise<HoldDecision>;
171
+ /** The page gave up the held request, or its frame went away: nothing was sent. */
172
+ lost(): void;
173
+ /** The attachment ended. */
174
+ close(): void;
175
+ private end;
176
+ whenHeld(timeoutMs?: number): Promise<HeldCardRequest | null>;
177
+ cancel(reason?: string): Promise<void>;
178
+ release(input: HoldReleaseInput): Promise<HoldReleaseOutcome>;
179
+ private decide;
180
+ }
package/dist/hold.js ADDED
@@ -0,0 +1,270 @@
1
+ // The card request hold (attachToCdp's `hold` mode; CARD-REQUEST-HOLD.md). A page's card request is paused in
2
+ // the browser's network layer and HELD: no approval exists, nothing reaches the processor, and the request waits
3
+ // for one of two answers.
4
+ //
5
+ // release({ amount, currency, pageTotal }) the person confirmed this total, and the page shows it now: the
6
+ // approval is created only then, at that total. A live client answers the page with the device's replay; a
7
+ // sandbox client never sends the request on (the approval is created and read, nothing is charged).
8
+ // cancel(reason) the request fails in the page; nothing was created or sent.
9
+ //
10
+ // A held request that would outlive its processor's own wait (Braintree's client gives up after 60 s, Bolt's
11
+ // tokenizer after 20 s) is cancelled first, so a hold never ends in an unknown state. One request is held at a
12
+ // time; every other card request, and every one after a decision, fails at once.
13
+ //
14
+ // Beside the registry's recognisers, a hold also catches two kinds of request the SDK cannot pay:
15
+ // - Bolt's tokenizer: Bolt seals the card and its answer to a key pair made inside its own frame, so no device
16
+ // replay can answer it. Held, then only cancelled.
17
+ // - Any request whose body carries the placeholder card number (`placeholderNumbers`): a store's own card post or
18
+ // an unknown processor. The browser only ever holds the placeholder, so a body with its digits is a card request
19
+ // wherever it goes. In sandbox it may be released unchanged (it carries the documented test number only).
20
+ // The hold records the processor and the endpoint (origin and path, any path credential masked), never a body or a
21
+ // header.
22
+ import { redactUrl } from './client.js';
23
+ /**
24
+ * Bolt's embedded card frame (connect.bolt.com credit-card-input): a POST of `{ payload, nonce, public_key }` JSON,
25
+ * the card boxed to Bolt's key, to `<base>/token` or `<base>/token/cvv`, with a 20 s timeout and retries.
26
+ */
27
+ export const BOLT_HOLD_RULE = {
28
+ psp: 'bolt',
29
+ match: /^[a-z]+:\/\/(?:(?:production|sandbox|staging)\.bolttk\.com|tokenizer(?:-sandbox|-staging)?\.bolt(?:app)?\.com)(?::\d+)?\/token(?:\/cvv)?(?:[?#]|$)/i,
30
+ patterns: [{ urlPattern: '*://*.bolttk.com*/token*' }, { urlPattern: '*://tokenizer*.bolt.com*/token*' }, { urlPattern: '*://tokenizer*.boltapp.com*/token*' }],
31
+ kind: 'tokenize',
32
+ deadlineMs: 20_000,
33
+ };
34
+ /** Each recogniser's own wait for its card request, where it was measured; any other gets HOLD_DEFAULT_DEADLINE_MS. */
35
+ export const HOLD_DEADLINES_MS = { braintree: 60_000, adyen: 60_000, bolt: 20_000 };
36
+ export const HOLD_DEFAULT_DEADLINE_MS = 30_000;
37
+ /** How long before the processor's own wait the hold gives the request up. */
38
+ export const HOLD_DEADLINE_MARGIN_MS = 2_000;
39
+ /** The resource types a page can send a card in: XHR, fetch, a form post, and the rest. */
40
+ const PLACEHOLDER_RESOURCE_TYPES = ['XHR', 'Fetch', 'Document', 'Other', 'Ping'];
41
+ /**
42
+ * Any request whose body carries one of the placeholder card numbers, wherever it goes. It cannot see a card a
43
+ * processor's frame encrypts first (Bolt, Adyen): those need their own rule. Null without a usable number.
44
+ */
45
+ export function placeholderHoldRule(numbers, deadlineMs = HOLD_DEFAULT_DEADLINE_MS) {
46
+ const digits = numbers.map((n) => String(n).replace(/\D/g, '')).filter((d) => d.length >= 12 && d.length <= 19);
47
+ if (!digits.length)
48
+ return null;
49
+ // The digits as a page may send them: plain, or grouped with spaces, dashes, '+' or '%20'.
50
+ const sep = '(?:\\s|-|\\+|%20)?';
51
+ const body = new RegExp(digits.map((d) => d.split('').join(sep)).join('|'));
52
+ return {
53
+ psp: 'placeholder_card', match: /^[a-z]+:\/\//i, body, kind: 'store_card_post', deadlineMs, methods: ['POST', 'PUT', 'PATCH'],
54
+ patterns: PLACEHOLDER_RESOURCE_TYPES.map((resourceType) => ({ urlPattern: '*', resourceType })),
55
+ };
56
+ }
57
+ /** The hold-only rule a request is a card request under, or null. */
58
+ export function matchHoldOnlyRule(request, rules) {
59
+ for (const rule of rules) {
60
+ if (!(rule.methods ?? ['POST']).includes(String(request.method).toUpperCase()))
61
+ continue;
62
+ if (!rule.match.test(request.url))
63
+ continue;
64
+ if (rule.body && !rule.body.test(request.body ?? ''))
65
+ continue;
66
+ return rule;
67
+ }
68
+ return null;
69
+ }
70
+ /** A hold ended without a release: the paused request fails, and every later card request fails at once. */
71
+ export class HoldEndedError extends Error {
72
+ reason;
73
+ constructor(reason) {
74
+ super(`card request hold ended: ${reason}`);
75
+ this.reason = reason;
76
+ this.name = 'HoldEndedError';
77
+ }
78
+ }
79
+ /** A second card request while one is held: this request alone fails, the held one and the checkout are untouched. */
80
+ export class HoldRefusedError extends Error {
81
+ constructor() { super('another card request is held'); this.name = 'HoldRefusedError'; }
82
+ }
83
+ /** A sandbox release answered: the approval was created and read, and the request was never sent on. */
84
+ export class HoldAnsweredError extends Error {
85
+ outcome;
86
+ constructor(outcome) {
87
+ super(`card request hold answered without sending: ${outcome.outcome}`);
88
+ this.outcome = outcome;
89
+ this.name = 'HoldAnsweredError';
90
+ }
91
+ }
92
+ /**
93
+ * A held request's origin and path as redactUrl reads them, so a credential a processor writes into the path (GoPay's
94
+ * card form token, a PayMongo session) never reaches an event or onHeld.
95
+ */
96
+ function endpointOf(url) {
97
+ try {
98
+ new URL(url);
99
+ }
100
+ catch {
101
+ return redactUrl(String(url)).slice(0, 120);
102
+ }
103
+ return redactUrl(url);
104
+ }
105
+ function validTotal(t) {
106
+ const v = t;
107
+ return !!v && Number.isSafeInteger(v.amount) && v.amount > 0 && typeof v.currency === 'string' && /^[A-Za-z]{3}$/.test(v.currency);
108
+ }
109
+ /**
110
+ * The hold's state, shared by the transport and the caller. The transport calls `hold` when a card request pauses
111
+ * and awaits the decision; the caller calls `release` or `cancel`. Not exported from the package: the caller sees
112
+ * the CardRequestHold face.
113
+ */
114
+ export class HoldGate {
115
+ options;
116
+ sandbox;
117
+ emit;
118
+ _state = 'watching';
119
+ _held = null;
120
+ _refused = 0;
121
+ timer = null;
122
+ waiters = [];
123
+ pending = null;
124
+ rules;
125
+ constructor(options, sandbox, emit) {
126
+ this.options = options;
127
+ this.sandbox = sandbox;
128
+ this.emit = emit;
129
+ const placeholder = options.placeholderNumbers?.length ? placeholderHoldRule(options.placeholderNumbers) : null;
130
+ this.rules = [BOLT_HOLD_RULE, ...(placeholder ? [placeholder] : [])];
131
+ }
132
+ get state() { return this._state; }
133
+ get held() { return this._held ? { ...this._held } : null; }
134
+ get refused() { return this._refused; }
135
+ /** Fetch patterns the hold-only rules need beside the registry's. */
136
+ patterns() { return this.rules.flatMap((r) => r.patterns); }
137
+ deadlineFor(psp, rule) {
138
+ return this.options.deadlineMs ?? rule?.deadlineMs ?? (psp ? HOLD_DEADLINES_MS[psp] : undefined) ?? HOLD_DEFAULT_DEADLINE_MS;
139
+ }
140
+ /** A card request that may not be held now (a second one, or one after a decision): counted, and failed by the caller. */
141
+ refuse(psp, url) {
142
+ if (this._state === 'watching')
143
+ return false;
144
+ this._refused++;
145
+ this.emit('card_request_refused', { psp, endpoint: endpointOf(url), state: this._state });
146
+ return true;
147
+ }
148
+ /**
149
+ * Hold a card request: resolves with the person's decision, or rejects with HoldEndedError when the hold is
150
+ * cancelled, expires, loses its request or closes. The caller (the transport) answers or fails the request.
151
+ */
152
+ hold(input) {
153
+ if (this._state !== 'watching')
154
+ return Promise.reject(new HoldEndedError(`not_watching:${this._state}`));
155
+ const deadlineMs = this.deadlineFor(input.psp, input.rule);
156
+ this._held = { psp: input.psp, endpoint: endpointOf(input.url), kind: input.kind, replayable: input.replayable, heldAt: Date.now(), deadlineMs };
157
+ this._state = 'held';
158
+ const decision = new Promise((resolve, reject) => { this.pending = { resolve, reject }; });
159
+ decision.catch(() => undefined);
160
+ const margin = this.options.deadlineMarginMs ?? HOLD_DEADLINE_MARGIN_MS;
161
+ this.timer = setTimeout(() => {
162
+ if (this._state !== 'held')
163
+ return;
164
+ this.end('expired', 'expired');
165
+ this.emit('card_request_expired', { endpoint: this._held?.endpoint, afterMs: Date.now() - (this._held?.heldAt ?? Date.now()) });
166
+ }, Math.max(0, deadlineMs - margin));
167
+ this.timer.unref?.();
168
+ const held = this.held;
169
+ this.emit('card_request_held', held);
170
+ try {
171
+ this.options.onHeld?.(held);
172
+ }
173
+ catch { /* observer only */ }
174
+ for (const w of this.waiters.splice(0))
175
+ w(held);
176
+ return decision;
177
+ }
178
+ /** The page gave up the held request, or its frame went away: nothing was sent. */
179
+ lost() {
180
+ if (this._state !== 'held')
181
+ return;
182
+ this.end('lost', 'request_lost');
183
+ this.emit('card_request_lost', { endpoint: this._held?.endpoint });
184
+ }
185
+ /** The attachment ended. */
186
+ close() {
187
+ if (this._state === 'held')
188
+ this.end('closed', 'closed');
189
+ else if (this._state === 'watching')
190
+ this._state = 'closed';
191
+ for (const w of this.waiters.splice(0))
192
+ w(null);
193
+ }
194
+ end(state, reason) {
195
+ this._state = state;
196
+ if (this.timer) {
197
+ clearTimeout(this.timer);
198
+ this.timer = null;
199
+ }
200
+ const p = this.pending;
201
+ this.pending = null;
202
+ p?.reject(new HoldEndedError(reason));
203
+ for (const w of this.waiters.splice(0))
204
+ w(null);
205
+ }
206
+ whenHeld(timeoutMs = 30_000) {
207
+ if (this._state === 'held')
208
+ return Promise.resolve(this.held);
209
+ if (this._state !== 'watching')
210
+ return Promise.resolve(null);
211
+ return new Promise((resolve) => {
212
+ const t = setTimeout(() => resolve(null), timeoutMs);
213
+ t.unref?.();
214
+ this.waiters.push((h) => { clearTimeout(t); resolve(h); });
215
+ });
216
+ }
217
+ async cancel(reason = 'cancelled') {
218
+ if (this._state === 'watching') {
219
+ this._state = 'cancelled';
220
+ this.emit('card_request_hold_cancelled', { reason, held: false });
221
+ for (const w of this.waiters.splice(0))
222
+ w(null);
223
+ return;
224
+ }
225
+ if (this._state !== 'held')
226
+ return;
227
+ this.end('cancelled', reason);
228
+ this.emit('card_request_cancelled', { reason });
229
+ }
230
+ async release(input) {
231
+ if (this._state !== 'held' || !this.pending || !this._held)
232
+ return { outcome: 'failed', detail: `not_held:${this._state}` };
233
+ const fail = async (detail) => { await this.cancel(detail); return { outcome: 'failed', detail }; };
234
+ if (!validTotal(input))
235
+ return fail('invalid_amount');
236
+ // The exact total, or nothing: a page that moved (tax, shipping) after the person confirmed is not released.
237
+ const page = input.pageTotal;
238
+ if (!validTotal(page))
239
+ return fail('total_unread');
240
+ if (page.currency.toLowerCase() !== input.currency.toLowerCase())
241
+ return fail('total_changed');
242
+ if (input.upTo ? page.amount > input.amount : page.amount !== input.amount)
243
+ return fail(input.upTo ? 'total_over_ceiling' : 'total_changed');
244
+ const held = this._held;
245
+ let settle;
246
+ const settled = new Promise((resolve) => { settle = resolve; });
247
+ if (!held.replayable) {
248
+ if (held.kind === 'store_card_post' && input.continuePlaceholder && this.sandbox) {
249
+ this.decide({ kind: 'continue', settle });
250
+ this.emit('card_request_released', { psp: held.psp, continued: true, amount: input.amount, currency: input.currency.toLowerCase(), upTo: !!input.upTo });
251
+ return settled;
252
+ }
253
+ // Bolt's sealed tokenizer and a store's own card post have no device replay: the request is cancelled.
254
+ return fail(held.kind === 'store_card_post' ? (this.sandbox ? 'store_card_post_needs_continue' : 'store_card_post_unreviewed') : 'not_replayable');
255
+ }
256
+ this.decide({ kind: 'release', amount: input.amount, currency: input.currency.toLowerCase(), settle });
257
+ this.emit('card_request_released', { psp: held.psp, amount: input.amount, currency: input.currency.toLowerCase(), upTo: !!input.upTo });
258
+ return settled;
259
+ }
260
+ decide(decision) {
261
+ this._state = 'released';
262
+ if (this.timer) {
263
+ clearTimeout(this.timer);
264
+ this.timer = null;
265
+ }
266
+ const p = this.pending;
267
+ this.pending = null;
268
+ p?.resolve(decision);
269
+ }
270
+ }
@@ -0,0 +1,4 @@
1
+ /** Whether this processor's hosted page is entered with a GET this build reads exactly. */
2
+ export declare function hasHostedGetEntry(psp: string): boolean;
3
+ /** Whether `url` is exactly the GET entry of this processor's hosted page (false for a processor with none). */
4
+ export declare function isHostedGetEntry(psp: string, url: string): boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Hosted payment pages whose entry is a plain GET: the processors whose merchant sends the buyer's browser to the
3
+ * processor's own page with a link (a redirect, or a page or frame location set by script) rather than a form post.
4
+ *
5
+ * Such an entry carries no card. The cardholder's device takes it over (mode hosted_form): it makes the processor's
6
+ * own calls for the payment the link names, and the paused navigation gets this SDK's submitted_on_device notice.
7
+ * A GET recognizer otherwise reads as a card request carried in the URL (Worldpay eProtect's registration), which
8
+ * this build completes only for a grammar it checks, so a hosted page joins this table with the exact link
9
+ * payment-core's resolveRecognizerForUrl admits for it, and nothing else: a URL the table does not admit is never
10
+ * paused, as the API would refuse it, and the page's own request goes on unchanged.
11
+ *
12
+ * Each rule is dark on its own: a processor is paused only once the API serves its recognizer, which it does only
13
+ * for a build that declares the processor's capability (client.ts SUPPORTED_CAPABILITIES) and a company Agentcard
14
+ * turned the processor on for. A rule listed here for a processor this build does not declare pauses nothing.
15
+ */
16
+ import { PRZELEWY24_PSP, isPrzelewy24EntryUrl } from './przelewy24.js';
17
+ import { COMGATE_PSP, isComgateEntryUrl } from './comgate.js';
18
+ import { TRANSBANK_PSP, isTransbankEntryUrl } from './transbank.js';
19
+ const HOSTED_GET_ENTRIES = new Map([
20
+ [PRZELEWY24_PSP, isPrzelewy24EntryUrl],
21
+ [COMGATE_PSP, isComgateEntryUrl],
22
+ [TRANSBANK_PSP, isTransbankEntryUrl],
23
+ ]);
24
+ /** Whether this processor's hosted page is entered with a GET this build reads exactly. */
25
+ export function hasHostedGetEntry(psp) {
26
+ return HOSTED_GET_ENTRIES.has(psp);
27
+ }
28
+ /** Whether `url` is exactly the GET entry of this processor's hosted page (false for a processor with none). */
29
+ export function isHostedGetEntry(psp, url) {
30
+ const rule = HOSTED_GET_ENTRIES.get(psp);
31
+ return !!rule && typeof url === 'string' && rule(url) === true;
32
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export type { CheckoutAttachOptions } from './agentcard.js';
3
3
  export { CardEncryptedError, UnsupportedModeError, UnsupportedCapabilityError, PreparationRequiredError, AmountRequiredError, PageAmountRequiredError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, CheckoutAuthError, redactUrl, SUPPORTED_MODES, SUPPORTED_CAPABILITIES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, AdyenTestPlatformRefusedError, MerchantTotalError, PlaceholderCardError, } from './client.js';
4
4
  export type { GmoUnsupportedReason } from './gmo.js';
5
5
  export type { DatatransRefusalReason } from './datatrans.js';
6
+ export type { TokenexIframeRefusalReason } from './tokenex-iframe.js';
6
7
  export type { PelecardRefusalReason } from './pelecard.js';
7
8
  export type { PrimerUnsupportedReason, PrimerBlockedReason } from './primer.js';
8
9
  export { CONVERGE_REFUSAL_REASONS } from './elavon-converge.js';
@@ -13,6 +14,8 @@ export type { SecureAcceptanceCheckoutRefusal, SecureAcceptanceUnknownReason } f
13
14
  export type { PausedRequest, ReplayResponse, TokenReplay, CseReplay, MerchantHostedReplay, VerifoneReplay, HostedFormReplay, CardEndpointReplay, DeviceEncryptReplay, AmountAuthority, AuthorizeInput, VaultClientOptions, AgentcardOptions, AdyenTestPlatformRefusal, MerchantTotalRefusal, PrepareCheckoutOptions, PrepareCheckoutInput, PreparedCheckout, RazorpayProcessorError, RuntimeCancelReason, ExecutionMetadata, PlaceholderCard, } from './client.js';
14
15
  export type { MicroformField, MicroformSidecar } from './cybersource-flex.js';
15
16
  export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
17
+ export { BOLT_HOLD_RULE, HOLD_DEADLINES_MS, HOLD_DEFAULT_DEADLINE_MS, HOLD_DEADLINE_MARGIN_MS, HoldAnsweredError, HoldEndedError, matchHoldOnlyRule, placeholderHoldRule } from './hold.js';
18
+ export type { CardRequestHold, HeldCardRequest, HoldOnlyRule, HoldOptions, HoldReleaseInput, HoldReleaseOutcome, HoldState } from './hold.js';
16
19
  export { CheckoutAttachmentError } from './attachment.js';
17
20
  export { EbanxFollowUpError } from './ebanx.js';
18
21
  export { CardEndpointRefusedError, cardEndpointPatterns } from './card-endpoint.js';
@@ -42,7 +45,7 @@ export { VoiceModule, checkoutOriginOf, CANCEL_REASONS, bundleOf, streamAddress,
42
45
  export type { VoiceOptions, VoiceTransport } from './voice.js';
43
46
  export type { ApiTransport } from './api-module.js';
44
47
  export { BuyModule, BuyError, isBuyDeclined, isBuyNeedsApproval, isBuyVoiceUnsupported, BUY_TURN_TIMEOUT_MS, VOICE_EXECUTION_UNSUPPORTED } from './buy.js';
45
- export type { BuyStatus, BuyApproval, BuyVoiceDeclineCode, BuyCart, BuyCartLine, BuyDeliveryAddress, BuyPaymentSource, BuyOutcome, BuyPlacement, BuyCatalog, BuyUnmatched, BuyTurn, BuyDeclined, BuyNeedsApproval, BuyVoiceUnsupported, BuyVoiceTurn, BuyOrder, BuyLastCheckout, BuyConversation, BuyAskInput, BuyConfirmInput, BuyApproveByVoiceInput, BuyConversationInput, } from './buy.js';
48
+ export type { BuyStatus, BuyApproval, BuyVoiceDeclineCode, BuyCart, BuyCartLine, BuyDeliveryAddress, BuyPaymentSource, BuyOutcome, BuyPlacement, BuyCatalog, BuyUnmatched, BuyTurn, BuyDeclined, BuyNeedsApproval, BuyVoiceUnsupported, BuyVoiceTurn, BuyOrder, BuyLastCheckout, BuyConversation, BuyAskInput, BuyConfirmInput, BuyApproveByVoiceInput, BuyConversationInput, BuyCheckoutHandoff, BuyOpenOffer, BuyBrowserCheckout, BuyApiAmount, } from './buy.js';
46
49
  export { Languages } from './languages.js';
47
50
  export type { Language, LanguageInfo } from './languages.js';
48
51
  export { verifyWebhook, signWebhook, parseWebhookEvent, parseSignatureHeader, isCheckoutAuthorizationEvent, isVaultEvent, CHECKOUT_AUTHORIZATION_EVENT_TYPES, VAULT_EVENT_TYPES } from './webhooks.js';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { Agentcard, Agentcard as VaultClient, CheckoutModule } from './agentcard
4
4
  export { CardEncryptedError, UnsupportedModeError, UnsupportedCapabilityError, PreparationRequiredError, AmountRequiredError, PageAmountRequiredError, ApprovalTimeoutError, ApprovalDeclinedError, AmountMismatchError, PresetRefusedError, PRESET_REFUSAL_REASONS, IntentNotConfirmableError, ProcessorRefusedError, CheckoutApiError, CheckoutAuthError, redactUrl, SUPPORTED_MODES, SUPPORTED_CAPABILITIES, PaymentOutcomeUnknownError, CheckoutCancelledError, CheckoutPreparationError, AdyenTestPlatformRefusedError, MerchantTotalError, PlaceholderCardError, } from './client.js';
5
5
  export { CONVERGE_REFUSAL_REASONS } from './elavon-converge.js';
6
6
  export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
7
+ export { BOLT_HOLD_RULE, HOLD_DEADLINES_MS, HOLD_DEFAULT_DEADLINE_MS, HOLD_DEADLINE_MARGIN_MS, HoldAnsweredError, HoldEndedError, matchHoldOnlyRule, placeholderHoldRule } from './hold.js';
7
8
  export { CheckoutAttachmentError } from './attachment.js';
8
9
  // EBANX after its card token: why a send an approved token request covers got no answer from the cardholder's device.
9
10
  export { EbanxFollowUpError } from './ebanx.js';
@@ -6,6 +6,7 @@ import type { GmoUnsupportedReason } from './gmo.js';
6
6
  import type { ShijiStsUnsupportedReason } from './shiji-sts.js';
7
7
  import type { PaddleVgsPayRefusal } from './paddle-vgs-pay.js';
8
8
  import type { PaypalCardFormReason } from './paypal-card-forms.js';
9
+ import { type CardRequestHold, type HoldReleaseInput, type HoldReleaseOutcome } from './hold.js';
9
10
  /** The application's resolver confirmed the payment for this checkout authorization. */
10
11
  export interface MerchantPaymentConfirmation {
11
12
  kind: 'merchant_payment';
@@ -130,6 +131,10 @@ export interface CheckoutController {
130
131
  retryAfterMerchantFailure(result: {
131
132
  status: 'failed';
132
133
  }): void;
134
+ /** Hold mode (`attachToCdp(…, { hold: true })`): the held card request and its answers; null otherwise. */
135
+ readonly hold?: CardRequestHold | null;
136
+ /** Hold mode: `controller.hold.release(input)`. Rejects on an attachment without a hold. */
137
+ release?(input: HoldReleaseInput): Promise<HoldReleaseOutcome>;
133
138
  }
134
139
  /** How many times one approval's card-free Stripe confirmation may be checked before it is simply held. */
135
140
  export declare const MAX_STRIPE_CONTINUATION_CHECKS = 3;
@@ -167,11 +172,16 @@ export declare class CheckoutLifecycle implements CheckoutController {
167
172
  private preparationHandler?;
168
173
  private preparationUsed;
169
174
  private resumeHandler;
175
+ private holdHandle;
170
176
  readonly abort: AbortController;
171
177
  constructor(options: LifecycleOptions);
172
178
  getState(): Readonly<CheckoutState>;
173
179
  setPreparationHandler(handler: (options: PrepareCheckoutOptions) => Promise<PreparedCheckout>): void;
174
180
  prepare(options: PrepareCheckoutOptions): Promise<PreparedCheckout>;
181
+ /** Hold mode: the attachment's hold (hold.ts). */
182
+ setHold(hold: CardRequestHold): void;
183
+ get hold(): CardRequestHold | null;
184
+ release(input: HoldReleaseInput): Promise<HoldReleaseOutcome>;
175
185
  /** The transport's hand while one request is paused for the business; null once it is answered. */
176
186
  setResumeHandler(handler: ((result: ResumeResult) => Promise<void>) | null): void;
177
187
  resume(result: ResumeResult): Promise<void>;
package/dist/lifecycle.js CHANGED
@@ -2,6 +2,7 @@ import { AdyenTestPlatformRefusedError, ApprovalDeclinedError, ApprovalTimeoutEr
2
2
  import { CardEndpointRefusedError } from './card-endpoint.js';
3
3
  import { parseOwnedShopReceipt, OWNED_SHOP_SKU, OWNED_SHOP_AMOUNT_CENTS, OWNED_SHOP_CURRENCY } from './owned-shop.generated.js';
4
4
  import { isWspayChallenge } from './wspay.js';
5
+ import { HoldAnsweredError, HoldEndedError } from './hold.js';
5
6
  /**
6
7
  * The statuses that end an attempt: its outcome is known (`completed`, `declined`, `failed`,
7
8
  * `timed_out`, `unsupported`, `cancelled`), or the SDK already reported it unknown with its own
@@ -72,6 +73,7 @@ export class CheckoutLifecycle {
72
73
  preparationHandler;
73
74
  preparationUsed = false;
74
75
  resumeHandler = null;
76
+ holdHandle = null;
75
77
  abort = new AbortController();
76
78
  constructor(options) {
77
79
  this.options = options;
@@ -85,6 +87,14 @@ export class CheckoutLifecycle {
85
87
  return Promise.reject(new CheckoutPreparationError(null, 'transport_unavailable'));
86
88
  return this.preparationHandler(options);
87
89
  }
90
+ /** Hold mode: the attachment's hold (hold.ts). */
91
+ setHold(hold) { this.holdHandle = hold; }
92
+ get hold() { return this.holdHandle; }
93
+ release(input) {
94
+ if (!this.holdHandle)
95
+ return Promise.reject(new Error('No card request hold: attach with hold: true.'));
96
+ return this.holdHandle.release(input);
97
+ }
88
98
  /** The transport's hand while one request is paused for the business; null once it is answered. */
89
99
  setResumeHandler(handler) { this.resumeHandler = handler; }
90
100
  resume(result) {
@@ -450,6 +460,17 @@ export class CheckoutLifecycle {
450
460
  this.held = true;
451
461
  this.set({ ...this.state, status: error.reason === 'not_enabled' ? 'unsupported' : 'failed', reason: `merchant_card_endpoint_${error.reason}` });
452
462
  }
463
+ else if (error instanceof HoldEndedError) {
464
+ // Nothing was created or sent; the hold refuses every later card request.
465
+ this.held = true;
466
+ this.set({ ...this.state, status: error.reason === 'expired' ? 'timed_out' : 'cancelled', reason: `hold_${error.reason}` });
467
+ }
468
+ else if (error instanceof HoldAnsweredError) {
469
+ // Sandbox: the approval was created and read, and the held request was never sent on.
470
+ const o = error.outcome.outcome;
471
+ this.held = true;
472
+ this.set({ ...this.state, status: o === 'declined' ? 'declined' : o === 'expired' ? 'timed_out' : o === 'approved_not_charged' ? 'cancelled' : 'failed', reason: `hold_${o}` });
473
+ }
453
474
  else if (error instanceof CheckoutCancelledError) {
454
475
  this.cancel();
455
476
  }