@agent-cards/sdk 0.23.18 → 0.23.20

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 (49) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/PREFLIGHT.md +2 -0
  3. package/README.md +46 -7
  4. package/dist/aci.d.ts +0 -7
  5. package/dist/aci.js +0 -19
  6. package/dist/amadeus.d.ts +4 -2
  7. package/dist/amadeus.js +16 -3
  8. package/dist/buy.d.ts +84 -2
  9. package/dist/buy.js +16 -1
  10. package/dist/card-endpoint-facts.d.ts +2 -1
  11. package/dist/card-endpoint-facts.js +18 -9
  12. package/dist/card-endpoint.d.ts +20 -4
  13. package/dist/card-endpoint.js +38 -4
  14. package/dist/card-fields.generated.js +974 -45
  15. package/dist/cdp.d.ts +29 -0
  16. package/dist/cdp.js +302 -18
  17. package/dist/client.d.ts +43 -5
  18. package/dist/client.js +237 -43
  19. package/dist/datatrans.d.ts +2 -2
  20. package/dist/datatrans.generated.js +16 -11
  21. package/dist/datatrans.js +2 -2
  22. package/dist/hipay.generated.d.ts +56 -0
  23. package/dist/hipay.generated.js +389 -0
  24. package/dist/hold.d.ts +180 -0
  25. package/dist/hold.js +264 -0
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +1 -0
  28. package/dist/lifecycle.d.ts +10 -0
  29. package/dist/lifecycle.js +21 -0
  30. package/dist/merchant-card-endpoints.generated.js +102 -11
  31. package/dist/paddle-vgs.generated.d.ts +3 -2
  32. package/dist/paddle-vgs.generated.js +9 -5
  33. package/dist/pelecard.generated.d.ts +2 -1
  34. package/dist/pelecard.generated.js +14 -5
  35. package/dist/pre-claim.d.ts +14 -0
  36. package/dist/pre-claim.js +76 -3
  37. package/dist/prepared-processor.d.ts +6 -2
  38. package/dist/prepared-processor.js +18 -2
  39. package/dist/primer.generated.js +998 -69
  40. package/dist/quickpay.d.ts +44 -0
  41. package/dist/quickpay.js +65 -0
  42. package/dist/stripe-checkout.generated.js +320 -2
  43. package/dist/twoc2p.d.ts +54 -0
  44. package/dist/twoc2p.generated.d.ts +33 -0
  45. package/dist/twoc2p.generated.js +1145 -0
  46. package/dist/twoc2p.js +85 -0
  47. package/dist/voice.d.ts +16 -13
  48. package/dist/voice.js +13 -10
  49. package/package.json +3 -3
package/dist/hold.js ADDED
@@ -0,0 +1,264 @@
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), never a body or a header.
21
+ /**
22
+ * Bolt's embedded card frame (connect.bolt.com credit-card-input): a POST of `{ payload, nonce, public_key }` JSON,
23
+ * the card boxed to Bolt's key, to `<base>/token` or `<base>/token/cvv`, with a 20 s timeout and retries.
24
+ */
25
+ export const BOLT_HOLD_RULE = {
26
+ psp: 'bolt',
27
+ match: /^[a-z]+:\/\/(?:(?:production|sandbox|staging)\.bolttk\.com|tokenizer(?:-sandbox|-staging)?\.bolt(?:app)?\.com)(?::\d+)?\/token(?:\/cvv)?(?:[?#]|$)/i,
28
+ patterns: [{ urlPattern: '*://*.bolttk.com*/token*' }, { urlPattern: '*://tokenizer*.bolt.com*/token*' }, { urlPattern: '*://tokenizer*.boltapp.com*/token*' }],
29
+ kind: 'tokenize',
30
+ deadlineMs: 20_000,
31
+ };
32
+ /** Each recogniser's own wait for its card request, where it was measured; any other gets HOLD_DEFAULT_DEADLINE_MS. */
33
+ export const HOLD_DEADLINES_MS = { braintree: 60_000, adyen: 60_000, bolt: 20_000 };
34
+ export const HOLD_DEFAULT_DEADLINE_MS = 30_000;
35
+ /** How long before the processor's own wait the hold gives the request up. */
36
+ export const HOLD_DEADLINE_MARGIN_MS = 2_000;
37
+ /** The resource types a page can send a card in: XHR, fetch, a form post, and the rest. */
38
+ const PLACEHOLDER_RESOURCE_TYPES = ['XHR', 'Fetch', 'Document', 'Other', 'Ping'];
39
+ /**
40
+ * Any request whose body carries one of the placeholder card numbers, wherever it goes. It cannot see a card a
41
+ * processor's frame encrypts first (Bolt, Adyen): those need their own rule. Null without a usable number.
42
+ */
43
+ export function placeholderHoldRule(numbers, deadlineMs = HOLD_DEFAULT_DEADLINE_MS) {
44
+ const digits = numbers.map((n) => String(n).replace(/\D/g, '')).filter((d) => d.length >= 12 && d.length <= 19);
45
+ if (!digits.length)
46
+ return null;
47
+ // The digits as a page may send them: plain, or grouped with spaces, dashes, '+' or '%20'.
48
+ const sep = '(?:\\s|-|\\+|%20)?';
49
+ const body = new RegExp(digits.map((d) => d.split('').join(sep)).join('|'));
50
+ return {
51
+ psp: 'placeholder_card', match: /^[a-z]+:\/\//i, body, kind: 'store_card_post', deadlineMs, methods: ['POST', 'PUT', 'PATCH'],
52
+ patterns: PLACEHOLDER_RESOURCE_TYPES.map((resourceType) => ({ urlPattern: '*', resourceType })),
53
+ };
54
+ }
55
+ /** The hold-only rule a request is a card request under, or null. */
56
+ export function matchHoldOnlyRule(request, rules) {
57
+ for (const rule of rules) {
58
+ if (!(rule.methods ?? ['POST']).includes(String(request.method).toUpperCase()))
59
+ continue;
60
+ if (!rule.match.test(request.url))
61
+ continue;
62
+ if (rule.body && !rule.body.test(request.body ?? ''))
63
+ continue;
64
+ return rule;
65
+ }
66
+ return null;
67
+ }
68
+ /** A hold ended without a release: the paused request fails, and every later card request fails at once. */
69
+ export class HoldEndedError extends Error {
70
+ reason;
71
+ constructor(reason) {
72
+ super(`card request hold ended: ${reason}`);
73
+ this.reason = reason;
74
+ this.name = 'HoldEndedError';
75
+ }
76
+ }
77
+ /** A second card request while one is held: this request alone fails, the held one and the checkout are untouched. */
78
+ export class HoldRefusedError extends Error {
79
+ constructor() { super('another card request is held'); this.name = 'HoldRefusedError'; }
80
+ }
81
+ /** A sandbox release answered: the approval was created and read, and the request was never sent on. */
82
+ export class HoldAnsweredError extends Error {
83
+ outcome;
84
+ constructor(outcome) {
85
+ super(`card request hold answered without sending: ${outcome.outcome}`);
86
+ this.outcome = outcome;
87
+ this.name = 'HoldAnsweredError';
88
+ }
89
+ }
90
+ function endpointOf(url) {
91
+ try {
92
+ const u = new URL(url);
93
+ return `${u.origin}${u.pathname}`;
94
+ }
95
+ catch {
96
+ return String(url).slice(0, 120);
97
+ }
98
+ }
99
+ function validTotal(t) {
100
+ const v = t;
101
+ return !!v && Number.isSafeInteger(v.amount) && v.amount > 0 && typeof v.currency === 'string' && /^[A-Za-z]{3}$/.test(v.currency);
102
+ }
103
+ /**
104
+ * The hold's state, shared by the transport and the caller. The transport calls `hold` when a card request pauses
105
+ * and awaits the decision; the caller calls `release` or `cancel`. Not exported from the package: the caller sees
106
+ * the CardRequestHold face.
107
+ */
108
+ export class HoldGate {
109
+ options;
110
+ sandbox;
111
+ emit;
112
+ _state = 'watching';
113
+ _held = null;
114
+ _refused = 0;
115
+ timer = null;
116
+ waiters = [];
117
+ pending = null;
118
+ rules;
119
+ constructor(options, sandbox, emit) {
120
+ this.options = options;
121
+ this.sandbox = sandbox;
122
+ this.emit = emit;
123
+ const placeholder = options.placeholderNumbers?.length ? placeholderHoldRule(options.placeholderNumbers) : null;
124
+ this.rules = [BOLT_HOLD_RULE, ...(placeholder ? [placeholder] : [])];
125
+ }
126
+ get state() { return this._state; }
127
+ get held() { return this._held ? { ...this._held } : null; }
128
+ get refused() { return this._refused; }
129
+ /** Fetch patterns the hold-only rules need beside the registry's. */
130
+ patterns() { return this.rules.flatMap((r) => r.patterns); }
131
+ deadlineFor(psp, rule) {
132
+ return this.options.deadlineMs ?? rule?.deadlineMs ?? (psp ? HOLD_DEADLINES_MS[psp] : undefined) ?? HOLD_DEFAULT_DEADLINE_MS;
133
+ }
134
+ /** A card request that may not be held now (a second one, or one after a decision): counted, and failed by the caller. */
135
+ refuse(psp, url) {
136
+ if (this._state === 'watching')
137
+ return false;
138
+ this._refused++;
139
+ this.emit('card_request_refused', { psp, endpoint: endpointOf(url), state: this._state });
140
+ return true;
141
+ }
142
+ /**
143
+ * Hold a card request: resolves with the person's decision, or rejects with HoldEndedError when the hold is
144
+ * cancelled, expires, loses its request or closes. The caller (the transport) answers or fails the request.
145
+ */
146
+ hold(input) {
147
+ if (this._state !== 'watching')
148
+ return Promise.reject(new HoldEndedError(`not_watching:${this._state}`));
149
+ const deadlineMs = this.deadlineFor(input.psp, input.rule);
150
+ this._held = { psp: input.psp, endpoint: endpointOf(input.url), kind: input.kind, replayable: input.replayable, heldAt: Date.now(), deadlineMs };
151
+ this._state = 'held';
152
+ const decision = new Promise((resolve, reject) => { this.pending = { resolve, reject }; });
153
+ decision.catch(() => undefined);
154
+ const margin = this.options.deadlineMarginMs ?? HOLD_DEADLINE_MARGIN_MS;
155
+ this.timer = setTimeout(() => {
156
+ if (this._state !== 'held')
157
+ return;
158
+ this.end('expired', 'expired');
159
+ this.emit('card_request_expired', { endpoint: this._held?.endpoint, afterMs: Date.now() - (this._held?.heldAt ?? Date.now()) });
160
+ }, Math.max(0, deadlineMs - margin));
161
+ this.timer.unref?.();
162
+ const held = this.held;
163
+ this.emit('card_request_held', held);
164
+ try {
165
+ this.options.onHeld?.(held);
166
+ }
167
+ catch { /* observer only */ }
168
+ for (const w of this.waiters.splice(0))
169
+ w(held);
170
+ return decision;
171
+ }
172
+ /** The page gave up the held request, or its frame went away: nothing was sent. */
173
+ lost() {
174
+ if (this._state !== 'held')
175
+ return;
176
+ this.end('lost', 'request_lost');
177
+ this.emit('card_request_lost', { endpoint: this._held?.endpoint });
178
+ }
179
+ /** The attachment ended. */
180
+ close() {
181
+ if (this._state === 'held')
182
+ this.end('closed', 'closed');
183
+ else if (this._state === 'watching')
184
+ this._state = 'closed';
185
+ for (const w of this.waiters.splice(0))
186
+ w(null);
187
+ }
188
+ end(state, reason) {
189
+ this._state = state;
190
+ if (this.timer) {
191
+ clearTimeout(this.timer);
192
+ this.timer = null;
193
+ }
194
+ const p = this.pending;
195
+ this.pending = null;
196
+ p?.reject(new HoldEndedError(reason));
197
+ for (const w of this.waiters.splice(0))
198
+ w(null);
199
+ }
200
+ whenHeld(timeoutMs = 30_000) {
201
+ if (this._state === 'held')
202
+ return Promise.resolve(this.held);
203
+ if (this._state !== 'watching')
204
+ return Promise.resolve(null);
205
+ return new Promise((resolve) => {
206
+ const t = setTimeout(() => resolve(null), timeoutMs);
207
+ t.unref?.();
208
+ this.waiters.push((h) => { clearTimeout(t); resolve(h); });
209
+ });
210
+ }
211
+ async cancel(reason = 'cancelled') {
212
+ if (this._state === 'watching') {
213
+ this._state = 'cancelled';
214
+ this.emit('card_request_hold_cancelled', { reason, held: false });
215
+ for (const w of this.waiters.splice(0))
216
+ w(null);
217
+ return;
218
+ }
219
+ if (this._state !== 'held')
220
+ return;
221
+ this.end('cancelled', reason);
222
+ this.emit('card_request_cancelled', { reason });
223
+ }
224
+ async release(input) {
225
+ if (this._state !== 'held' || !this.pending || !this._held)
226
+ return { outcome: 'failed', detail: `not_held:${this._state}` };
227
+ const fail = async (detail) => { await this.cancel(detail); return { outcome: 'failed', detail }; };
228
+ if (!validTotal(input))
229
+ return fail('invalid_amount');
230
+ // The exact total, or nothing: a page that moved (tax, shipping) after the person confirmed is not released.
231
+ const page = input.pageTotal;
232
+ if (!validTotal(page))
233
+ return fail('total_unread');
234
+ if (page.currency.toLowerCase() !== input.currency.toLowerCase())
235
+ return fail('total_changed');
236
+ if (input.upTo ? page.amount > input.amount : page.amount !== input.amount)
237
+ return fail(input.upTo ? 'total_over_ceiling' : 'total_changed');
238
+ const held = this._held;
239
+ let settle;
240
+ const settled = new Promise((resolve) => { settle = resolve; });
241
+ if (!held.replayable) {
242
+ if (held.kind === 'store_card_post' && input.continuePlaceholder && this.sandbox) {
243
+ this.decide({ kind: 'continue', settle });
244
+ this.emit('card_request_released', { psp: held.psp, continued: true, amount: input.amount, currency: input.currency.toLowerCase(), upTo: !!input.upTo });
245
+ return settled;
246
+ }
247
+ // Bolt's sealed tokenizer and a store's own card post have no device replay: the request is cancelled.
248
+ return fail(held.kind === 'store_card_post' ? (this.sandbox ? 'store_card_post_needs_continue' : 'store_card_post_unreviewed') : 'not_replayable');
249
+ }
250
+ this.decide({ kind: 'release', amount: input.amount, currency: input.currency.toLowerCase(), settle });
251
+ this.emit('card_request_released', { psp: held.psp, amount: input.amount, currency: input.currency.toLowerCase(), upTo: !!input.upTo });
252
+ return settled;
253
+ }
254
+ decide(decision) {
255
+ this._state = 'released';
256
+ if (this.timer) {
257
+ clearTimeout(this.timer);
258
+ this.timer = null;
259
+ }
260
+ const p = this.pending;
261
+ this.pending = null;
262
+ p?.resolve(decision);
263
+ }
264
+ }
package/dist/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export type { SecureAcceptanceCheckoutRefusal, SecureAcceptanceUnknownReason } f
13
13
  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
14
  export type { MicroformField, MicroformSidecar } from './cybersource-flex.js';
15
15
  export { attachToCdp, attachToPlaywright, corsHeadersFor, corsDecision, withCorsHeaders } from './cdp.js';
16
+ export { BOLT_HOLD_RULE, HOLD_DEADLINES_MS, HOLD_DEFAULT_DEADLINE_MS, HOLD_DEADLINE_MARGIN_MS, HoldAnsweredError, HoldEndedError, matchHoldOnlyRule, placeholderHoldRule } from './hold.js';
17
+ export type { CardRequestHold, HeldCardRequest, HoldOnlyRule, HoldOptions, HoldReleaseInput, HoldReleaseOutcome, HoldState } from './hold.js';
16
18
  export { CheckoutAttachmentError } from './attachment.js';
17
19
  export { EbanxFollowUpError } from './ebanx.js';
18
20
  export { CardEndpointRefusedError, cardEndpointPatterns } from './card-endpoint.js';
@@ -42,7 +44,7 @@ export { VoiceModule, checkoutOriginOf, CANCEL_REASONS, bundleOf, streamAddress,
42
44
  export type { VoiceOptions, VoiceTransport } from './voice.js';
43
45
  export type { ApiTransport } from './api-module.js';
44
46
  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';
47
+ 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
48
  export { Languages } from './languages.js';
47
49
  export type { Language, LanguageInfo } from './languages.js';
48
50
  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
  }
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 527edb8fd48229e7404d5fda8d1f8e243b5694c350eee105bec8815a0e848df0
3
+ // artifact-sha256: 7b5770020fc598b9f1fdfc842ef761812943269a1bd368d4751f99c1ffad7a17
4
4
  // src/json-path.js
5
5
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
6
6
  function isPlainObject(v) {
@@ -644,7 +644,6 @@ var NAVIGATION_HEADERS = /* @__PURE__ */ Object.freeze([
644
644
  "sec-fetch-site",
645
645
  "sec-fetch-user"
646
646
  ]);
647
- var GUARANTEE_RATES = "a policy for guarantee rates: the hotel charges the card at the stay, by when a single-use card may have closed";
648
647
  var ATT_SESSION = /* @__PURE__ */ deepFreeze({
649
648
  cookies: true,
650
649
  cookie_names: null,
@@ -2108,7 +2107,9 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
2108
2107
  "secure-hotel-booking.com sits behind DataDome. The same card frame serves every D-EDGE hotel, so the profile names D-EDGE rather than one hotel; its currency is HUMA's.",
2109
2108
  "HUMA's own payment step, read live Oct 6, 2026 (live-checks-oct6/huma_kotor_bay): the frame's cleanCardNumber rewrites CardNumber on every keypress and on blur into groups of four with one space between them for a Visa or Mastercard number (4242 4242 4242 4242; 4-6-5 for an Amex, 4-6-4 for a Diners), so the post carries the spaces.",
2110
2109
  "cleanCardNumber strips every non-digit and writes the number in the format of the first CARD_TYPES row whose pattern it matches (the frame's inline script, the same in the archive.org copies and live on Oct 6): amex /^3[47]/ format_465, the first 15 digits as 4-6-5; diners_club_international /^36/ and diners_club_carte_blanche /^30[0-5]/ format_464, the first 14 digits as 4-6-4; every other row (visa, master, jcb, maestro, discover, instapayment, diners_club /^54/), and a number no row matches, format_4444, groups of four with the rest last. card.pan is grouped_by_type: the rules take the number grouped that way or as plain digits, and write the real number the way the placeholder was written, a grouped one in its own network's grouping, so a real American Express card behind a placeholder grouped in fours still goes out 4-6-5. Behind a grouped placeholder, a real 34 or 37 number of any length but 15, or a 36 or 300 to 305 number of any length but 14, is refused: the frame has no grouping for it and cuts a longer one short.",
2111
- "The same check read when HUMA charges: its Non Refundable rate charges the whole stay at this post (the hub shows Due to pay now and its button reads PAY), while its flexible rate only guarantees the booking with the card, which the hotel charges on a late cancellation or a no-show."
2110
+ "The same check read when HUMA charges: its Non Refundable rate charges the whole stay at this post (the hub shows Due to pay now and its button reads PAY), while its flexible rate only guarantees the booking with the card, which the hotel charges on a late cancellation or a no-show.",
2111
+ "The hub loads the booking it takes a payment for with POST /load/<session uuid> on pay.secure-hotel-booking.com and spreads the answer into its state (main.bundle.js v=1.0.3573, sha256 eb06043d...51897c9, the payment reducer; read live Oct 6, 2026 as a POST answered 200). It also sends 3-D Secure browser data to /setDevice/<session uuid>, which changes nothing the record holds, and calls /Voucher/add, /Voucher/remove, /Loyalty/set, /Loyalty/remove and /Issuer/set/<session uuid> only when the guest applies a voucher, points or a bank, each answer replacing the record. Every hub URL ends in the session uuid the card post's path carries.",
2112
+ "The /load answer (archive.org copies from other D-EDGE hotels, Dec 2025 to Sep 2026): deposit (due now), total and balance (the room and the part left for the stay, both without the stay tax the basket lines add), hasOriginalDeposit (false at a rate that only guarantees the booking), hasScheduledPayment, isFullDiscount, vouchers and status (New while unpaid), amounts written in the booking's language ('€ 1,609.36', '116,10 €'). facts reads it with the dedge_pay_hub reader, so the agent's amount is held to the deposit, and a guarantee, a scheduled charge or part of the room left for the stay is refused (Felipe's rule of Oct 7, 2026: a hotel is paid only at a rate charged in full at booking)."
2112
2113
  ],
2113
2114
  har: null,
2114
2115
  checkout: "https://card.secure-hotel-booking.com",
@@ -2146,7 +2147,15 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
2146
2147
  response: { headers: ["content-type", "location", "set-cookie"], withhold: [] },
2147
2148
  resends: [],
2148
2149
  challenge: null,
2149
- facts: null,
2150
+ // The booking the hub takes the payment for lives on the hub's own host, not the card frame's.
2151
+ facts: {
2152
+ reader: "dedge_pay_hub",
2153
+ origin: "https://pay.secure-hotel-booking.com",
2154
+ responses: [{ method: "POST", path: "/load/:session", query: {}, format: "json" }],
2155
+ // 3-D Secure browser data, sent beside /load when the hub loads: the record does not change.
2156
+ inert: [{ method: "POST", path: "/setDevice/:session", query: {} }],
2157
+ bind: "session"
2158
+ },
2150
2159
  bootstrap: "/robots.txt",
2151
2160
  timeout_ms: 6e4,
2152
2161
  charge_timing: "at_request",
@@ -2155,7 +2164,7 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
2155
2164
  "HUMA's own payment step: whether its pay hub offers this card frame or a WSPay redirect (a HAR of one real booking's payment step)",
2156
2165
  "which cookies the card post needs, with DataDome in front of the host",
2157
2166
  "the answer to the card post (a redirect, a 3DS step or a page), and whether a 3DS step sends the card again",
2158
- GUARANTEE_RATES,
2167
+ "HUMA's /load answer for its prepaid rate, as a recorded purchase shows it: the deposit, total and balance strings, a zero balance beside the stay tax, and the status of an unpaid booking that facts reads",
2159
2168
  "that the bootstrap path answers without a server error"
2160
2169
  ]
2161
2170
  },
@@ -2321,6 +2330,8 @@ var MERCHANT_CARD_ENDPOINT_PROFILES = /* @__PURE__ */ deepFreeze([
2321
2330
  challenge: { at: ["captcha", "token"], ttl_ms: 24e4 },
2322
2331
  facts: {
2323
2332
  reader: "fastspring_session",
2333
+ // The record's answers come from the popup's own store host, the endpoint.
2334
+ origin: null,
2324
2335
  // Every answer FastSpring's page reads its order from (refreshPageResponse and
2325
2336
  // refreshOrderResponse): the popup document, the one-segment calls (/tax, /coupons,
2326
2337
  // /locale, /exempt), an item's quantity, renewal or add-on change, and a verified discount.
@@ -3019,10 +3030,73 @@ function fastspringFacts(state, merchant) {
3019
3030
  return fail(unreadable);
3020
3031
  }
3021
3032
  const recurring = projection.hasRecurring || projection.forceFuture || anySelectedRenewal(groups);
3022
- return { ok: true, facts: { amount: { amountCents, currency: currency.toLowerCase() }, live: projection.production, recurring } };
3033
+ return { ok: true, facts: { amount: { amountCents, currency: currency.toLowerCase() }, live: projection.production, recurring, deferred: null } };
3034
+ }
3035
+ var DEDGE_MAX_VOUCHERS = 8;
3036
+ var EURO_FIRST = /^\u20ac (\d{1,3}(?:,\d{3}){0,2})(?:\.(\d{2}))?$/;
3037
+ var EURO_LAST = /^(\d{1,3}(?:[ .\u00a0\u202f]\d{3}){0,2})(?:,(\d{2}))? \u20ac$/;
3038
+ function hubAmount(text) {
3039
+ if (typeof text !== "string")
3040
+ return null;
3041
+ const match = EURO_FIRST.exec(text) ?? EURO_LAST.exec(text);
3042
+ if (!match)
3043
+ return /\d/.test(text) && !/\u20ac|EUR/i.test(text) ? { other: true } : null;
3044
+ if (new Set(match[1].replace(/\d/g, "")).size > 1)
3045
+ return null;
3046
+ return { cents: Number(match[1].replace(/\D/g, "")) * 100 + Number(match[2] ?? "0") };
3047
+ }
3048
+ function dedgeState(format, text) {
3049
+ const state = format === "json" ? parseJson(text) : void 0;
3050
+ return isPlainObject(state) ? { state } : { refusal: "the payment page's booking record is not a JSON object" };
3051
+ }
3052
+ function dedgeProjection(state) {
3053
+ if (!isPlainObject(state))
3054
+ return null;
3055
+ const vouchers = state.vouchers === void 0 ? [] : state.vouchers;
3056
+ if (!Array.isArray(vouchers) || vouchers.length > DEDGE_MAX_VOUCHERS)
3057
+ return null;
3058
+ const scalar = (value) => value !== null && typeof value === "object" ? null : value;
3059
+ return {
3060
+ status: scalar(state.status),
3061
+ deposit: scalar(state.deposit),
3062
+ total: scalar(state.total),
3063
+ balance: scalar(state.balance),
3064
+ hasOriginalDeposit: scalar(state.hasOriginalDeposit),
3065
+ hasScheduledPayment: scalar(state.hasScheduledPayment),
3066
+ isFullDiscount: scalar(state.isFullDiscount),
3067
+ paidAmount: state.paidAmount === void 0 ? "" : scalar(state.paidAmount),
3068
+ vouchers: vouchers.map(() => ({}))
3069
+ };
3070
+ }
3071
+ function dedgeFacts(state, merchant) {
3072
+ const unreadable = `${merchant}'s order record is not in the layout Agentcard reviewed`;
3073
+ const projection = isPlainObject(state) ? dedgeProjection(state) : null;
3074
+ if (!projection || JSON.stringify(projection) !== JSON.stringify(state))
3075
+ return fail(unreadable);
3076
+ const { status, hasOriginalDeposit, hasScheduledPayment, isFullDiscount, paidAmount, vouchers } = projection;
3077
+ if (typeof status !== "string" || typeof hasOriginalDeposit !== "boolean" || typeof hasScheduledPayment !== "boolean" || typeof isFullDiscount !== "boolean" || typeof paidAmount !== "string")
3078
+ return fail(unreadable);
3079
+ if (status !== "New")
3080
+ return fail(`${merchant}'s order is not waiting for a payment`);
3081
+ if (isFullDiscount)
3082
+ return fail(`vouchers or points pay the whole of ${merchant}'s order, so the card pays nothing`);
3083
+ if (paidAmount !== "")
3084
+ return fail(`part of ${merchant}'s order was already paid another way`);
3085
+ if (vouchers.length > 0)
3086
+ return fail(`a voucher pays part of ${merchant}'s order, which Agentcard has not reviewed`);
3087
+ const [deposit, total, balance] = [projection.deposit, projection.total, projection.balance].map(hubAmount);
3088
+ if ([deposit, total, balance].some((amount) => amount?.other))
3089
+ return fail(`${merchant}'s order is in a currency this build does not read`);
3090
+ if (!deposit || !total || !balance)
3091
+ return fail(unreadable);
3092
+ if (deposit.cents + balance.cents !== total.cents)
3093
+ return fail(unreadable);
3094
+ const deferred = !hasOriginalDeposit || deposit.cents === 0 ? "guarantee" : hasScheduledPayment ? "scheduled" : balance.cents > 0 ? "balance" : null;
3095
+ return { ok: true, facts: { amount: { amountCents: deposit.cents, currency: "eur" }, live: true, recurring: false, deferred } };
3023
3096
  }
3024
3097
  var READERS = /* @__PURE__ */ Object.freeze({
3025
- fastspring_session: Object.freeze({ state: fastspringState, project: fastspringProjection, facts: fastspringFacts })
3098
+ fastspring_session: Object.freeze({ state: fastspringState, project: fastspringProjection, facts: fastspringFacts }),
3099
+ dedge_pay_hub: Object.freeze({ state: dedgeState, project: dedgeProjection, facts: dedgeFacts })
3026
3100
  });
3027
3101
  var MERCHANT_CARD_ENDPOINT_FACT_READERS = /* @__PURE__ */ Object.freeze(Object.keys(READERS));
3028
3102
  function cardPlaceholders(p, cardUrl) {
@@ -3040,12 +3114,13 @@ function boundShape(p, card, shapes, exchange) {
3040
3114
  }
3041
3115
  return null;
3042
3116
  }
3117
+ var recordOrigin = (p) => p.facts.origin ?? p.endpoint;
3043
3118
  function shapeFor(p, cardUrl, exchange) {
3044
3119
  if (!p.facts || !isPlainObject(exchange) || typeof exchange.method !== "string" || typeof exchange.url !== "string")
3045
3120
  return null;
3046
3121
  const card = cardPlaceholders(p, cardUrl);
3047
3122
  const url = strictUrl(exchange.url);
3048
- if (!card || !url || url.origin !== p.endpoint)
3123
+ if (!card || !url || url.origin !== recordOrigin(p))
3049
3124
  return null;
3050
3125
  return boundShape(p, card, p.facts.responses, exchange);
3051
3126
  }
@@ -3057,12 +3132,19 @@ function sessionPath(p, card) {
3057
3132
  const written = segments.slice(0, at + 1).map((segment) => segment.startsWith(":") ? card[segment.slice(1)] : segment);
3058
3133
  return written.every((segment) => typeof segment === "string") ? written.join("/") : null;
3059
3134
  }
3135
+ function onSession(p, card, url) {
3136
+ if (p.facts.origin) {
3137
+ const value = card[p.facts.bind];
3138
+ return url.origin === p.facts.origin && typeof value === "string" && url.pathname.split("/").at(-1) === value;
3139
+ }
3140
+ const session = sessionPath(p, card);
3141
+ return !!session && url.origin === p.endpoint && (url.pathname === session || url.pathname.startsWith(`${session}/`));
3142
+ }
3060
3143
  function merchantCardEndpointFactsRole(profile, cardUrl, exchange) {
3061
3144
  const p = trustedMerchantCardEndpoint(profile);
3062
3145
  if (!p.facts || !isPlainObject(exchange) || typeof exchange.method !== "string" || typeof exchange.url !== "string")
3063
3146
  return null;
3064
3147
  const card = cardPlaceholders(p, cardUrl);
3065
- const session = card ? sessionPath(p, card) : null;
3066
3148
  let url;
3067
3149
  try {
3068
3150
  url = new URL(exchange.url);
@@ -3070,10 +3152,12 @@ function merchantCardEndpointFactsRole(profile, cardUrl, exchange) {
3070
3152
  catch {
3071
3153
  return null;
3072
3154
  }
3073
- if (!session || url.origin !== p.endpoint || url.pathname !== session && !url.pathname.startsWith(`${session}/`))
3155
+ if (!card)
3074
3156
  return null;
3075
3157
  if (exchange.method.toUpperCase() === p.method && exchange.url === cardUrl)
3076
3158
  return "card";
3159
+ if (!onSession(p, card, url))
3160
+ return null;
3077
3161
  if (shapeFor(p, cardUrl, exchange))
3078
3162
  return "record";
3079
3163
  if (boundShape(p, card, p.facts.inert, exchange))
@@ -3110,13 +3194,20 @@ function readMerchantCardEndpointFacts(profile, cardUrl, record) {
3110
3194
  return fail(`the order record is not ${p.merchant}'s record of this checkout`);
3111
3195
  return READERS[p.facts.reader].facts(record.state, p.merchant);
3112
3196
  }
3197
+ var DEFERRED_REASONS = /* @__PURE__ */ Object.freeze({
3198
+ guarantee: (merchant) => `${merchant} only holds the card as a guarantee for this order and charges it later, when a single-use card may have closed; choose a rate paid in full now`,
3199
+ scheduled: (merchant) => `${merchant} charges this order on a later date, when a single-use card may have closed; choose a rate paid in full now`,
3200
+ balance: (merchant) => `${merchant} leaves part of this order to pay at the stay, when a single-use card may have closed; choose a rate paid in full now`
3201
+ });
3113
3202
  function merchantCardEndpointFactsRefusal(profile, facts, agent) {
3114
3203
  const p = trustedMerchantCardEndpoint(profile);
3115
3204
  const refuse2 = (code, reason) => ({ code, reason });
3116
- if (!isPlainObject(facts) || !isPlainObject(facts.amount) || !isPlainObject(agent))
3205
+ if (!isPlainObject(facts) || !isPlainObject(facts.amount) || !isPlainObject(agent) || facts.deferred !== null && !Object.hasOwn(DEFERRED_REASONS, facts.deferred))
3117
3206
  return refuse2("malformed", `the order record for ${p.merchant} is malformed`);
3118
3207
  if (facts.recurring !== false)
3119
3208
  return refuse2("recurring", `${p.merchant}'s order renews on a schedule, and each renewal would charge the card with no approval of its own`);
3209
+ if (facts.deferred !== null)
3210
+ return refuse2("deferred", DEFERRED_REASONS[facts.deferred](p.merchant));
3120
3211
  if (facts.live !== agent.live)
3121
3212
  return agent.live ? refuse2("test_checkout", `${p.merchant}'s checkout is in test mode`) : refuse2("live_checkout", `${p.merchant}'s checkout is live`);
3122
3213
  if (typeof agent.currency !== "string" || facts.amount.currency !== agent.currency.toLowerCase()) {
@@ -17,9 +17,10 @@ declare function paddleVgsBodyVerdict(body: any): Readonly<{
17
17
  }>;
18
18
  declare function paddleVgsRequestRefusal(request: any): any;
19
19
  declare function isPreparedPaddleVgsRequest(url: any, method: any, body: any, environment: any, headers: any): boolean;
20
+ declare var PADDLE_VGS_ANY_SELLER: boolean;
20
21
  declare var PADDLE_VGS_MERCHANT_PINS: readonly any[];
21
22
  declare function paddleVgsMerchantPin(merchantOrigin: any, pins?: readonly any[]): any;
22
- declare function paddleVgsMerchantRefusal(environment: any, merchantOrigin: any, authorization: any, pins?: readonly any[]): "unsupported_environment" | "paddle_vgs_merchant_unreviewed" | "paddle_vgs_seller_unreviewed" | null;
23
+ declare function paddleVgsMerchantRefusal(environment: any, merchantOrigin: any, authorization: any, pins?: readonly any[], anySeller?: boolean): "unsupported_environment" | "paddle_vgs_merchant_unreviewed" | "paddle_vgs_seller_unreviewed" | null;
23
24
  declare function paddleVgsSellerClaimsPinned(pins?: readonly any[]): boolean;
24
25
  declare function paddleVgsRefusalMessage(code: any): any;
25
26
  declare var PADDLE_VGS_CARD_NETWORKS: readonly string[];
@@ -75,4 +76,4 @@ declare function paddleVgsPayVerdict(request: any, approved: any): Readonly<{
75
76
  verdict: "continue";
76
77
  body: string;
77
78
  }>;
78
- export { PADDLE_VGS_CAPABILITY, PADDLE_VGS_CARDS_URLS, PADDLE_VGS_CARD_ID_WINDOW_MS, PADDLE_VGS_CARD_NETWORKS, PADDLE_VGS_CONTENT_TYPE, PADDLE_VGS_MERCHANT_PINS, PADDLE_VGS_OPERATION, PADDLE_VGS_PSP, PADDLE_VGS_REFUSAL_CODES, injectPaddleVgsCard, isPaddleVgsCardsUrl, isPreparedPaddleVgsRequest, paddleVgsAnswerOutcome, paddleVgsBodyVerdict, paddleVgsCardFacts, paddleVgsCardFactsAgree, paddleVgsCardFactsOf, paddleVgsCardRefusal, paddleVgsEnvironment, paddleVgsMerchantPin, paddleVgsMerchantRefusal, paddleVgsPayCarriesStoredCard, paddleVgsPayTarget, paddleVgsPayVerdict, paddleVgsRefusalCode, paddleVgsRefusalMessage, paddleVgsRequestRefusal, paddleVgsSellerClaimsPinned, paddleVgsStoredCardRefusal, paddleVgsTypedCardRefusal };
79
+ export { PADDLE_VGS_ANY_SELLER, PADDLE_VGS_CAPABILITY, PADDLE_VGS_CARDS_URLS, PADDLE_VGS_CARD_ID_WINDOW_MS, PADDLE_VGS_CARD_NETWORKS, PADDLE_VGS_CONTENT_TYPE, PADDLE_VGS_MERCHANT_PINS, PADDLE_VGS_OPERATION, PADDLE_VGS_PSP, PADDLE_VGS_REFUSAL_CODES, injectPaddleVgsCard, isPaddleVgsCardsUrl, isPreparedPaddleVgsRequest, paddleVgsAnswerOutcome, paddleVgsBodyVerdict, paddleVgsCardFacts, paddleVgsCardFactsAgree, paddleVgsCardFactsOf, paddleVgsCardRefusal, paddleVgsEnvironment, paddleVgsMerchantPin, paddleVgsMerchantRefusal, paddleVgsPayCarriesStoredCard, paddleVgsPayTarget, paddleVgsPayVerdict, paddleVgsRefusalCode, paddleVgsRefusalMessage, paddleVgsRequestRefusal, paddleVgsSellerClaimsPinned, paddleVgsStoredCardRefusal, paddleVgsTypedCardRefusal };