@agent-cards/sdk 0.23.23 → 0.23.24

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.
@@ -0,0 +1,239 @@
1
+ // Paddle's own checkout answers, recorded read-only in the agent's browser, for the create of a Paddle TokenEx /pay
2
+ // (payment-core paddle-checkout-terms.js reads them, for the API). Paddle's checkout service answers the frame for
3
+ // the checkout it is selling (POST /transaction-checkout, PATCH /transaction-checkout/<che_...>/, POST .../customer),
4
+ // and each answer states what the checkout charges today and, for a free trial, the renewal's price, cycle and first
5
+ // date. The /pay names none of that. So from attach on, the adapters tell this watch about every request to those
6
+ // URLs, each answer's status, the moment each finishes and each body once it is read; the watch keeps each body cut
7
+ // to the terms (projectPaddleCheckoutAnswer), never the customer, address or seller, and at a Billing /pay's pause
8
+ // hands the client the newest few as `paddleCheckout`, which it sends with the create.
9
+ //
10
+ // The browser reads an answer's text after it reports the answer finished, so a pause can come between the two. The
11
+ // pause then waits, up to PADDLE_CHECKOUT_READ_WAIT_MS, for the texts of the /pay's own checkout's answers that
12
+ // finished before it. When one of them is still not read after that, or was still loading at the pause, the terms
13
+ // the page had when the card left are not known, and no answers go: the API then reads no terms, never an older
14
+ // answer's in place of the newest. Answers about another checkout, or on the other environment's host, neither wait
15
+ // nor count; the create counts once its text names the /pay's checkout, since its URL names none.
16
+ //
17
+ // Each checkout keeps its own answers (PER_CHECKOUT, and each host's creates as one more such list), for the
18
+ // CHECKOUTS asked about most recently, so another checkout's requests never push this checkout's answers out. When
19
+ // the watch drops one of a checkout's answers, it remembers what that could hide, and a pause on that checkout sends
20
+ // no answers when a dropped one was still loading or finished no earlier than the newest it would send.
21
+ //
22
+ // What this proves: the answers are what this browser saw. The API holds the agent's amount to today's total they
23
+ // state, judges a renewal against the company's rules and admits a free trial's $0.00 only on them; an agent that
24
+ // writes its own answers is not stopped, a risk Felipe accepted by name for Paddle trials (backend config
25
+ // PADDLE_TRIAL_TERMS_RISK_ACCEPTED). Nothing here pauses, changes or answers a request.
26
+ import { PADDLE_CHECKOUT_ANSWERS_MAX, PADDLE_CHECKOUT_ANSWER_LIMIT, paddleCheckoutAnswerTarget, paddleTokenexPayTarget, projectPaddleCheckoutAnswer, } from './paddle-tokenex.generated.js';
27
+ /** How long a /pay's pause waits for the texts of Paddle's answers that finished before it. */
28
+ export const PADDLE_CHECKOUT_READ_WAIT_MS = 1_000;
29
+ const METHODS = ['GET', 'POST', 'PATCH', 'PUT'];
30
+ const MAX = PADDLE_CHECKOUT_ANSWERS_MAX;
31
+ const LIMIT = PADDLE_CHECKOUT_ANSWER_LIMIT;
32
+ /** Requests the watch keeps for one checkout, or for one host's creates, the oldest sent dropped first. */
33
+ const PER_CHECKOUT = 16;
34
+ /** Checkouts (each host's creates counting as one) the watch keeps requests for, the one asked about least recently dropped whole. */
35
+ const CHECKOUTS = 16;
36
+ /** Checkouts whose dropped answers the watch remembers (Lost), the oldest forgotten first. */
37
+ const LOST_KEPT = 64;
38
+ export class PaddleCheckoutWatch {
39
+ exchanges = new Map();
40
+ /** Each checkout's request keys, oldest sent first; the checkout asked about least recently first. */
41
+ checkouts = new Map();
42
+ /** Each checkout's dropped answers, by `${environment}:${checkoutId}`. */
43
+ lost = new Map();
44
+ /**
45
+ * By environment, the latest finish of a create dropped after it finished and before its text was read: it may have
46
+ * been any checkout's on that host, and a create is its checkout's oldest answer.
47
+ */
48
+ lostCreateFinishedAt = new Map();
49
+ waiters = new Set();
50
+ now;
51
+ readWaitMs;
52
+ constructor(options = {}) {
53
+ this.now = options.now ?? Date.now;
54
+ this.readWaitMs = options.readWaitMs ?? PADDLE_CHECKOUT_READ_WAIT_MS;
55
+ }
56
+ /** Whether the request under `key` is one of Paddle's checkout answers whose text is still to be read. */
57
+ tracks(key) {
58
+ return this.exchanges.get(key)?.body === null;
59
+ }
60
+ /** A request left the page: kept when it asks Paddle's checkout service about the checkout. */
61
+ sent(key, request) {
62
+ const method = String(request.method).toUpperCase();
63
+ const target = METHODS.includes(method) ? paddleCheckoutAnswerTarget(request.url) : null;
64
+ if (!target)
65
+ return;
66
+ // A key the browser reports again (a redirect) starts over.
67
+ this.remove(key);
68
+ const checkout = `${target.environment}:${target.checkoutId ?? ''}`;
69
+ this.exchanges.set(key, { method, url: request.url, checkout, sentAt: this.now(), status: null, finishedAt: null, body: null });
70
+ const keys = this.checkouts.get(checkout) ?? new Set();
71
+ this.checkouts.delete(checkout);
72
+ this.checkouts.set(checkout, keys.add(key));
73
+ while (keys.size > PER_CHECKOUT) {
74
+ const oldest = keys.values().next().value;
75
+ this.drop(oldest);
76
+ keys.delete(oldest);
77
+ }
78
+ while (this.checkouts.size > CHECKOUTS) {
79
+ const [oldest, oldestKeys] = this.checkouts.entries().next().value;
80
+ for (const dropped of [...oldestKeys])
81
+ this.drop(dropped);
82
+ this.checkouts.delete(oldest);
83
+ }
84
+ }
85
+ /** Drops a kept request, noting what it was about (forget). */
86
+ drop(key) {
87
+ const exchange = this.remove(key);
88
+ if (exchange)
89
+ this.forget(exchange);
90
+ }
91
+ /** Takes a kept request out without a trace, and hands it back. */
92
+ remove(key) {
93
+ const exchange = this.exchanges.get(key);
94
+ if (!exchange)
95
+ return undefined;
96
+ this.exchanges.delete(key);
97
+ const keys = this.checkouts.get(exchange.checkout);
98
+ keys?.delete(key);
99
+ if (keys && keys.size === 0)
100
+ this.checkouts.delete(exchange.checkout);
101
+ return exchange;
102
+ }
103
+ /** Notes what a dropped answer was about, so a pause on its checkout knows what it can no longer see. */
104
+ forget(exchange) {
105
+ const target = paddleCheckoutAnswerTarget(exchange.url);
106
+ if (!target)
107
+ return;
108
+ const id = target.checkoutId ?? checkoutNamed(exchange.body);
109
+ if (id === null) {
110
+ // A create still loading is another checkout's (the /pay names an id only a finished create gives); one that
111
+ // finished unread may have been any checkout's on its host.
112
+ if (exchange.finishedAt !== null && exchange.body === null) {
113
+ this.lostCreateFinishedAt.set(target.environment, Math.max(this.lostCreateFinishedAt.get(target.environment) ?? -Infinity, exchange.finishedAt));
114
+ }
115
+ return;
116
+ }
117
+ const key = `${target.environment}:${id}`;
118
+ const lost = this.lost.get(key) ?? { loadingSentAt: Infinity, finishedAt: -Infinity };
119
+ if (exchange.finishedAt === null)
120
+ lost.loadingSentAt = Math.min(lost.loadingSentAt, exchange.sentAt);
121
+ else
122
+ lost.finishedAt = Math.max(lost.finishedAt, exchange.finishedAt);
123
+ this.lost.delete(key);
124
+ this.lost.set(key, lost);
125
+ if (this.lost.size > LOST_KEPT)
126
+ this.lost.delete(this.lost.keys().next().value);
127
+ }
128
+ /** Its answer's status arrived. */
129
+ answered(key, status) {
130
+ const exchange = this.exchanges.get(key);
131
+ if (exchange && typeof status === 'number')
132
+ exchange.status = status;
133
+ }
134
+ /** It finished loading: stamped the moment the browser says so, before its text is read, so a pause waits for that read. */
135
+ finished(key, completedAt) {
136
+ const exchange = this.exchanges.get(key);
137
+ if (exchange)
138
+ exchange.finishedAt ??= completedAt;
139
+ }
140
+ /**
141
+ * Its text was read (`text`, or null when it could not be). Kept cut to the terms; a body that is not a checkout
142
+ * answer, or whose projection is longer than the API takes, is kept as an empty one, so the reader knows a newer
143
+ * answer it cannot read came in.
144
+ */
145
+ read(key, text, completedAt) {
146
+ const exchange = this.exchanges.get(key);
147
+ if (!exchange || exchange.body !== null)
148
+ return;
149
+ exchange.finishedAt ??= completedAt;
150
+ const body = typeof text === 'string' ? projectPaddleCheckoutAnswer(text) : null;
151
+ exchange.body = typeof body === 'string' && body.length <= LIMIT ? body : '';
152
+ this.settle();
153
+ }
154
+ /**
155
+ * The request failed or was given up. One that never finished gave the page no answer and is forgotten; one that
156
+ * finished gave the page an answer this watch could not read, and is kept as one that cannot be read.
157
+ */
158
+ failed(key) {
159
+ const exchange = this.exchanges.get(key);
160
+ if (!exchange)
161
+ return;
162
+ if (exchange.finishedAt === null)
163
+ this.remove(key);
164
+ else
165
+ exchange.body ??= '';
166
+ this.settle();
167
+ }
168
+ settle() {
169
+ for (const wake of [...this.waiters])
170
+ wake();
171
+ }
172
+ /**
173
+ * The answers for a Billing /pay paused at `pausedAt`, newest last, or undefined when there are none to send: the
174
+ * card request is not a Billing /pay, nothing was recorded for its checkout, or the terms as of the pause are not
175
+ * known (see the header). Waits, up to the read bound or until `signal` aborts, for the texts of the checkout's
176
+ * answers that finished before the pause.
177
+ */
178
+ async capture(pausedAt, cardUrl, signal) {
179
+ if (paddleTokenexPayTarget(cardUrl)?.line !== 'billing')
180
+ return undefined;
181
+ // The /pay's own checkout, read as the answer URL that checkout has: its id and its environment's host.
182
+ const pay = paddleCheckoutAnswerTarget(cardUrl.replace(/\/pay$/, ''));
183
+ if (!pay || pay.checkoutId === null)
184
+ return undefined;
185
+ const own = `${pay.environment}:${pay.checkoutId}`;
186
+ const creates = `${pay.environment}:`;
187
+ const byId = (exchange) => exchange.checkout === own;
188
+ const create = (exchange) => exchange.checkout === creates;
189
+ const kept = (checkout) => [...(this.checkouts.get(checkout) ?? [])].map((key) => this.exchanges.get(key));
190
+ const asked = () => [...kept(own), ...kept(creates)].filter((exchange) => exchange.sentAt <= pausedAt);
191
+ const unread = (exchange) => exchange.finishedAt !== null && exchange.finishedAt <= pausedAt && exchange.body === null;
192
+ // The wait runs on the real clock, whatever clock stamps the requests.
193
+ const deadline = Date.now() + this.readWaitMs;
194
+ while (asked().some(unread) && !signal?.aborted) {
195
+ const left = deadline - Date.now();
196
+ if (left <= 0)
197
+ break;
198
+ await new Promise((resolve) => {
199
+ const wake = () => { clearTimeout(timer); this.waiters.delete(wake); signal?.removeEventListener('abort', wake); resolve(); };
200
+ const timer = setTimeout(wake, left);
201
+ this.waiters.add(wake);
202
+ signal?.addEventListener('abort', wake, { once: true });
203
+ });
204
+ }
205
+ // An answer about the checkout still loading at the pause, still unread, or answered with no status: the terms as
206
+ // of the pause are not known. So too when a dropped one was still loading by then.
207
+ const lost = this.lost.get(own);
208
+ if (lost && lost.loadingSentAt <= pausedAt)
209
+ return undefined;
210
+ const checkout = asked().filter(byId);
211
+ if (checkout.some((exchange) => exchange.finishedAt === null || exchange.finishedAt > pausedAt || exchange.body === null || exchange.status === null))
212
+ return undefined;
213
+ // A create counts once it finished, was read and names this checkout: one still loading is another checkout's,
214
+ // since the /pay names an id only a finished create gives.
215
+ const created = asked().filter((exchange) => create(exchange) && exchange.status !== null && exchange.finishedAt !== null
216
+ && exchange.finishedAt <= pausedAt && checkoutNamed(exchange.body) === pay.checkoutId);
217
+ const answers = [...checkout, ...created].sort((a, b) => a.finishedAt - b.finishedAt).slice(-MAX)
218
+ .map((exchange) => ({ method: exchange.method, url: exchange.url, status: exchange.status, completedAt: exchange.finishedAt, body: exchange.body }));
219
+ // A dropped answer that finished no earlier than the newest kept one may have held the newest terms, and one that
220
+ // finished after the pause was still loading at it; every kept answer finished by the pause, so one test holds both.
221
+ // A create dropped unread on this host counts the same way, as it may have been this checkout's.
222
+ const newest = answers.at(-1)?.completedAt ?? -Infinity;
223
+ if ((lost && lost.finishedAt >= newest) || (this.lostCreateFinishedAt.get(pay.environment) ?? -Infinity) >= newest)
224
+ return undefined;
225
+ return answers.length ? { pausedAt, answers } : undefined;
226
+ }
227
+ }
228
+ /** The checkout a kept projection answers about, or null when it names none. */
229
+ function checkoutNamed(body) {
230
+ if (!body)
231
+ return null;
232
+ try {
233
+ const id = JSON.parse(body).data?.id;
234
+ return typeof id === 'string' ? id : null;
235
+ }
236
+ catch {
237
+ return null;
238
+ }
239
+ }
@@ -1,4 +1,32 @@
1
1
  declare function paddleVgsCardFactsAgree(facts: any, stored: any): boolean;
2
+ declare var PADDLE_CHECKOUT_ANSWERS_MAX: number;
3
+ declare var PADDLE_CHECKOUT_ANSWER_LIMIT: number;
4
+ declare var PADDLE_CHECKOUT_TERMS_REFUSALS: readonly string[];
5
+ declare function paddleMinorUnits(value: any, currency: any): number | null;
6
+ declare function paddleCheckoutAnswerTarget(url: any): Readonly<{
7
+ environment: "production" | "sandbox";
8
+ checkoutId: string;
9
+ leg: string;
10
+ }> | null;
11
+ declare function projectPaddleCheckoutAnswer(text: any): string | null;
12
+ declare function paddleCheckoutTerms(card: any, answers: any): Readonly<{
13
+ ok: false;
14
+ reason: any;
15
+ }> | Readonly<{
16
+ ok: true;
17
+ checkoutId: string;
18
+ environment: "production" | "sandbox";
19
+ currency: any;
20
+ todayCents: number;
21
+ renews: boolean;
22
+ recurringCents: number | null;
23
+ trial: Readonly<{
24
+ renewalCents: number;
25
+ interval: any;
26
+ frequency: any;
27
+ startsAt: any;
28
+ }> | null;
29
+ }>;
2
30
  declare var PADDLE_TOKENEX_PSP: string;
3
31
  declare var PADDLE_TOKENEX_CAPABILITY: string;
4
32
  declare var PADDLE_TOKENEX_OPERATION: string;
@@ -64,4 +92,4 @@ declare function paddleTokenexAnswerOutcome(status: any, text: any): {
64
92
  reason?: undefined;
65
93
  code?: undefined;
66
94
  };
67
- export { PADDLE_TOKENEX_CAPABILITY, PADDLE_TOKENEX_CARD_NETWORKS, PADDLE_TOKENEX_FACT_KEYS, PADDLE_TOKENEX_FIELDS, PADDLE_TOKENEX_OPERATION, PADDLE_TOKENEX_PSP, PADDLE_TOKENEX_REFUSAL_CODES, PADDLE_TOKENEX_SCHEME, encryptPaddleTokenexCard, isPaddleTokenexCardRequest, isPaddleTokenexCiphertext, isPaddleTokenexCiphertextUnderKey, paddleTokenexAnswerOutcome, paddleTokenexBodyVerdict, paddleVgsCardFactsAgree as paddleTokenexCardFactsAgree, paddleTokenexKeyUsable, paddleTokenexPayCarriesCard, paddleTokenexPayTarget, paddleTokenexRefusalCode, paddleTokenexRefusalMessage, paddleTokenexRequestRefusal, paddleTokenexStoredCardRefusal, paddleTokenexSubstitutionOf, paddleTokenexSubstitutionsRefusal, substitutePaddleTokenex };
95
+ export { PADDLE_CHECKOUT_ANSWERS_MAX, PADDLE_CHECKOUT_ANSWER_LIMIT, PADDLE_CHECKOUT_TERMS_REFUSALS, PADDLE_TOKENEX_CAPABILITY, PADDLE_TOKENEX_CARD_NETWORKS, PADDLE_TOKENEX_FACT_KEYS, PADDLE_TOKENEX_FIELDS, PADDLE_TOKENEX_OPERATION, PADDLE_TOKENEX_PSP, PADDLE_TOKENEX_REFUSAL_CODES, PADDLE_TOKENEX_SCHEME, encryptPaddleTokenexCard, isPaddleTokenexCardRequest, isPaddleTokenexCiphertext, isPaddleTokenexCiphertextUnderKey, paddleCheckoutAnswerTarget, paddleCheckoutTerms, paddleMinorUnits, paddleTokenexAnswerOutcome, paddleTokenexBodyVerdict, paddleVgsCardFactsAgree as paddleTokenexCardFactsAgree, paddleTokenexKeyUsable, paddleTokenexPayCarriesCard, paddleTokenexPayTarget, paddleTokenexRefusalCode, paddleTokenexRefusalMessage, paddleTokenexRequestRefusal, paddleTokenexStoredCardRefusal, paddleTokenexSubstitutionOf, paddleTokenexSubstitutionsRefusal, projectPaddleCheckoutAnswer, substitutePaddleTokenex };
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  // Generated from @agent-cards/payment-core. Do not edit.
3
- // artifact-sha256: 4289a22922e08251b579e9aadc396c8cff5d1a95b99ce35dd9a4618b43a33db5
3
+ // artifact-sha256: cb62940ac92ca9f2a61b5168f362003d67998f12d045b18d8bd07ae352e0cb3e
4
4
  // src/sha256.js
5
5
  var K = /* @__PURE__ */ new Uint32Array([
6
6
  1116352408,
@@ -617,6 +617,155 @@ function swapMerchantHostedFields(body, spec, ciphertext = ADYEN_JWE) {
617
617
  }
618
618
  return JSON.stringify(out);
619
619
  }
620
+ // src/paddle-checkout-terms.js
621
+ var PADDLE_CHECKOUT_ANSWERS_MAX = 8;
622
+ var PADDLE_CHECKOUT_ANSWER_LIMIT = 4096;
623
+ var PADDLE_CHECKOUT_TERMS_REFUSALS = /* @__PURE__ */ Object.freeze([
624
+ "paddle_checkout_not_billing",
625
+ "paddle_checkout_answers_missing",
626
+ "paddle_checkout_answer_unreadable",
627
+ "paddle_checkout_total_unreadable",
628
+ "paddle_checkout_credit_unsupported"
629
+ ]);
630
+ var ANSWER_URL = /^https:\/\/(sandbox-)?checkout-service\.paddle\.com\/transaction-checkout(?:\/([A-Za-z0-9_-]{1,64})(?:\/([a-z_]{1,32}))?)?\/?$/;
631
+ var PAY_URL = /^https:\/\/(sandbox-)?checkout-service\.paddle\.com\/transaction-checkout\/([A-Za-z0-9_-]{1,64})\/pay$/;
632
+ var NOT_ANSWERS = /* @__PURE__ */ new Set(["pay", "event"]);
633
+ var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PATCH", "PUT"]);
634
+ var CURRENCY = /^[A-Z]{3}$/;
635
+ var INTERVALS = /* @__PURE__ */ new Set(["day", "week", "month", "year"]);
636
+ var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/;
637
+ var ZERO_DECIMAL = /* @__PURE__ */ new Set(["BIF", "CLP", "DJF", "GNF", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "VND", "VUV", "XAF", "XOF", "XPF"]);
638
+ var THREE_DECIMAL = /* @__PURE__ */ new Set(["BHD", "JOD", "KWD", "OMR", "TND"]);
639
+ var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
640
+ var refuse2 = (reason) => Object.freeze({ ok: false, reason });
641
+ function paddleMinorUnits(value, currency) {
642
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || typeof currency !== "string" || !CURRENCY.test(currency))
643
+ return null;
644
+ const digits = ZERO_DECIMAL.has(currency) ? 0 : THREE_DECIMAL.has(currency) ? 3 : 2;
645
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(String(value));
646
+ if (!match || (match[2] ?? "").length > digits)
647
+ return null;
648
+ const minor = Number(match[1] + (match[2] ?? "").padEnd(digits, "0"));
649
+ return Number.isSafeInteger(minor) ? minor : null;
650
+ }
651
+ function paddleCheckoutAnswerTarget(url) {
652
+ const match = typeof url === "string" ? ANSWER_URL.exec(url) : null;
653
+ if (!match || match[3] && NOT_ANSWERS.has(match[3]) || match[2] && NOT_ANSWERS.has(match[2]) && !match[3])
654
+ return null;
655
+ return Object.freeze({ environment: match[1] ? "sandbox" : "production", checkoutId: match[2] ?? null, leg: match[3] ?? null });
656
+ }
657
+ var pickNumbers = (value, keys) => {
658
+ if (!record2(value))
659
+ return void 0;
660
+ const out = {};
661
+ for (const key of keys)
662
+ if (typeof value[key] === "number")
663
+ out[key] = value[key];
664
+ return out;
665
+ };
666
+ var pickPhase = (phase) => record2(phase) ? {
667
+ type: typeof phase.type === "string" ? phase.type.slice(0, 32) : null,
668
+ billing_cycle: record2(phase.billing_cycle) ? { interval: typeof phase.billing_cycle.interval === "string" ? phase.billing_cycle.interval.slice(0, 16) : null, frequency: phase.billing_cycle.frequency } : null,
669
+ starts_at: typeof phase.starts_at === "string" ? phase.starts_at.slice(0, 40) : null,
670
+ ends_at: typeof phase.ends_at === "string" ? phase.ends_at.slice(0, 40) : null,
671
+ total: phase.total,
672
+ currency_code: typeof phase.currency_code === "string" ? phase.currency_code.slice(0, 3) : null
673
+ } : null;
674
+ function projectPaddleCheckoutAnswer(text) {
675
+ let value;
676
+ try {
677
+ value = JSON.parse(text);
678
+ }
679
+ catch {
680
+ return null;
681
+ }
682
+ const data = record2(value) ? value.data : null;
683
+ if (!record2(data) || typeof data.id !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(data.id))
684
+ return null;
685
+ const projected = {
686
+ id: data.id,
687
+ environment: typeof data.environment === "string" ? data.environment.slice(0, 16) : null,
688
+ currency_code: typeof data.currency_code === "string" ? data.currency_code.slice(0, 3) : null,
689
+ totals: pickNumbers(data.totals, ["total", "balance", "credit"]) ?? null,
690
+ recurring_totals: pickNumbers(data.recurring_totals, ["total"]) ?? null,
691
+ includes_negative_option: typeof data.includes_negative_option === "boolean" ? data.includes_negative_option : null,
692
+ payment_breakdown: Array.isArray(data.payment_breakdown) ? data.payment_breakdown.slice(0, 8).map(pickPhase) : null
693
+ };
694
+ return JSON.stringify({ data: projected });
695
+ }
696
+ function paddleCheckoutTerms(card, answers) {
697
+ const pay = record2(card) && typeof card.url === "string" ? PAY_URL.exec(card.url) : null;
698
+ if (!pay)
699
+ return refuse2("paddle_checkout_not_billing");
700
+ const environment = pay[1] ? "sandbox" : "production";
701
+ const checkoutId = pay[2];
702
+ const pausedAt = record2(card) && Number.isFinite(card.pausedAt) ? card.pausedAt : Infinity;
703
+ let newest = null;
704
+ for (const answer of Array.isArray(answers) ? answers.slice(-PADDLE_CHECKOUT_ANSWERS_MAX) : []) {
705
+ if (!record2(answer) || typeof answer.method !== "string" || !METHODS.has(answer.method.toUpperCase()))
706
+ continue;
707
+ const target = paddleCheckoutAnswerTarget(answer.url);
708
+ if (!target || target.environment !== environment || target.checkoutId !== null && target.checkoutId !== checkoutId)
709
+ continue;
710
+ if (!Number.isFinite(answer.completedAt) || answer.completedAt > pausedAt)
711
+ continue;
712
+ const readable = Number.isInteger(answer.status) && answer.status >= 200 && answer.status < 300 && typeof answer.body === "string" && answer.body.length <= PADDLE_CHECKOUT_ANSWER_LIMIT;
713
+ const projected = readable ? projectPaddleCheckoutAnswer(answer.body) : null;
714
+ const data2 = projected ? JSON.parse(projected).data : null;
715
+ if (target.checkoutId === null && !(data2 && data2.id === checkoutId))
716
+ continue;
717
+ if (newest && answer.completedAt < newest.completedAt)
718
+ continue;
719
+ newest = { completedAt: answer.completedAt, data: data2 && data2.id === checkoutId && data2.environment === environment ? data2 : null };
720
+ }
721
+ if (!newest)
722
+ return refuse2("paddle_checkout_answers_missing");
723
+ const data = newest.data;
724
+ if (!data || typeof data.currency_code !== "string" || !CURRENCY.test(data.currency_code) || !record2(data.totals))
725
+ return refuse2("paddle_checkout_answer_unreadable");
726
+ const currency = data.currency_code;
727
+ const todayCents = paddleMinorUnits(data.totals.total, currency);
728
+ const balanceCents = paddleMinorUnits(data.totals.balance, currency);
729
+ if (todayCents === null || balanceCents === null)
730
+ return refuse2("paddle_checkout_total_unreadable");
731
+ if (balanceCents !== todayCents || data.totals.credit !== void 0 && data.totals.credit !== 0)
732
+ return refuse2("paddle_checkout_credit_unsupported");
733
+ const phases = Array.isArray(data.payment_breakdown) ? data.payment_breakdown : [];
734
+ const oneTime = data.recurring_totals === null && data.includes_negative_option === false && phases.every((phase) => record2(phase) && phase.type === "due_now" && phase.billing_cycle === null);
735
+ const recurringCents = record2(data.recurring_totals) ? paddleMinorUnits(data.recurring_totals.total, currency) : null;
736
+ return Object.freeze({
737
+ ok: true,
738
+ checkoutId,
739
+ environment,
740
+ currency,
741
+ todayCents,
742
+ renews: !oneTime,
743
+ recurringCents: oneTime ? null : recurringCents,
744
+ trial: oneTime ? null : freeTrialOf(phases, currency, todayCents, recurringCents)
745
+ });
746
+ }
747
+ function freeTrialOf(phases, currency, todayCents, recurringCents) {
748
+ if (phases.length !== 2 || todayCents !== 0)
749
+ return null;
750
+ const [trial, renewal] = phases;
751
+ if (!record2(trial) || !record2(renewal) || trial.type !== "free_trial" || renewal.type !== "normal")
752
+ return null;
753
+ if (trial.currency_code !== currency || renewal.currency_code !== currency)
754
+ return null;
755
+ if (paddleMinorUnits(trial.total, currency) !== 0)
756
+ return null;
757
+ const renewalCents = paddleMinorUnits(renewal.total, currency);
758
+ if (renewalCents === null || renewalCents <= 0 || renewalCents !== recurringCents)
759
+ return null;
760
+ const cycle = renewal.billing_cycle;
761
+ if (!record2(cycle) || !INTERVALS.has(cycle.interval) || !Number.isInteger(cycle.frequency) || cycle.frequency < 1 || cycle.frequency > 999)
762
+ return null;
763
+ if (typeof renewal.starts_at !== "string" || !INSTANT.test(renewal.starts_at) || trial.ends_at !== renewal.starts_at)
764
+ return null;
765
+ if (!Number.isFinite(Date.parse(renewal.starts_at)))
766
+ return null;
767
+ return Object.freeze({ renewalCents, interval: cycle.interval, frequency: cycle.frequency, startsAt: renewal.starts_at });
768
+ }
620
769
  // src/paddle-tokenex.js
621
770
  var PADDLE_TOKENEX_PSP = "paddle_tokenex";
622
771
  var PADDLE_TOKENEX_CAPABILITY = "paddle_tokenex_pay";
@@ -631,17 +780,17 @@ var PADDLE_TOKENEX_REFUSAL_CODES = /* @__PURE__ */ Object.freeze([
631
780
  "paddle_tokenex_card_mismatch",
632
781
  "paddle_tokenex_key_unavailable"
633
782
  ]);
634
- var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
783
+ var record3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
635
784
  var sameKeys = (value, keys) => {
636
785
  const own = Object.keys(value);
637
786
  return own.length === keys.length && own.every((key, at) => key === keys[at]);
638
787
  };
639
- var refuse2 = (code) => Object.freeze({ verdict: "refuse", code });
788
+ var refuse3 = (code) => Object.freeze({ verdict: "refuse", code });
640
789
  var CARD = /* @__PURE__ */ Object.freeze({ verdict: "card" });
641
790
  var CONTINUE = /* @__PURE__ */ Object.freeze({ verdict: "continue" });
642
- var PAY_URL = /^https:\/\/(sandbox-)?checkout-service\.paddle\.com\/(checkout|transaction-checkout)\/[A-Za-z0-9_-]{1,64}\/pay$/;
791
+ var PAY_URL2 = /^https:\/\/(sandbox-)?checkout-service\.paddle\.com\/(checkout|transaction-checkout)\/[A-Za-z0-9_-]{1,64}\/pay$/;
643
792
  function paddleTokenexPayTarget(url) {
644
- const match = typeof url === "string" ? PAY_URL.exec(url) : null;
793
+ const match = typeof url === "string" ? PAY_URL2.exec(url) : null;
645
794
  return match ? Object.freeze({ environment: match[1] ? "sandbox" : "production", line: match[2] === "checkout" ? "classic" : "billing" }) : null;
646
795
  }
647
796
  function paddleTokenexPayCarriesCard(body) {
@@ -706,29 +855,29 @@ function readPay(body, line, environment) {
706
855
  let value;
707
856
  try {
708
857
  value = JSON.parse(body);
709
- if (!record2(value) || JSON.stringify(value) !== body)
858
+ if (!record3(value) || JSON.stringify(value) !== body)
710
859
  return null;
711
860
  }
712
861
  catch {
713
862
  return null;
714
863
  }
715
864
  const data = value.data;
716
- if (!sameKeys(value, ["data"]) || !record2(data) || !(sameKeys(data, PAY_DATA_KEYS) || sameKeys(data, [...PAY_DATA_KEYS, "checkout"])) || data.payment_method_type !== (line === "classic" ? "CARD" : "card"))
865
+ if (!sameKeys(value, ["data"]) || !record3(data) || !(sameKeys(data, PAY_DATA_KEYS) || sameKeys(data, [...PAY_DATA_KEYS, "checkout"])) || data.payment_method_type !== (line === "classic" ? "CARD" : "card"))
717
866
  return null;
718
- if (data.checkout !== void 0 && !(record2(data.checkout) && sameKeys(data.checkout, ["frontend_host"]) && typeof data.checkout.frontend_host === "string" && FRONTEND_HOST.test(data.checkout.frontend_host)))
867
+ if (data.checkout !== void 0 && !(record3(data.checkout) && sameKeys(data.checkout, ["frontend_host"]) && typeof data.checkout.frontend_host === "string" && FRONTEND_HOST.test(data.checkout.frontend_host)))
719
868
  return null;
720
869
  const method = data.payment_method_data;
721
- const card = record2(method) && sameKeys(method, ["card"]) ? method.card : null;
722
- if (!record2(card) || !(sameKeys(card, PAY_CARD_KEYS) || sameKeys(card, [...PAY_CARD_KEYS, "session_tracking"])))
870
+ const card = record3(method) && sameKeys(method, ["card"]) ? method.card : null;
871
+ if (!record3(card) || !(sameKeys(card, PAY_CARD_KEYS) || sameKeys(card, [...PAY_CARD_KEYS, "session_tracking"])))
723
872
  return null;
724
873
  const metadata = card.metadata;
725
- if (!record2(metadata) || !sameKeys(metadata, METADATA_KEYS2) || typeof metadata.cardholder_name !== "string" || metadata.cardholder_name.length > NAME_LIMIT || CONTROL.test(metadata.cardholder_name) || cardLikeName(metadata.cardholder_name) || typeof metadata.card_first_six !== "string" || !FIRST_SIX2.test(metadata.card_first_six) || typeof metadata.card_last_four !== "string" || !LAST_FOUR2.test(metadata.card_last_four) || !expiryMonth2(metadata.expiry_month) || !expiryYear2(metadata.expiry_year) || !FRAME_BRANDS.includes(metadata.card_brand))
874
+ if (!record3(metadata) || !sameKeys(metadata, METADATA_KEYS2) || typeof metadata.cardholder_name !== "string" || metadata.cardholder_name.length > NAME_LIMIT || CONTROL.test(metadata.cardholder_name) || cardLikeName(metadata.cardholder_name) || typeof metadata.card_first_six !== "string" || !FIRST_SIX2.test(metadata.card_first_six) || typeof metadata.card_last_four !== "string" || !LAST_FOUR2.test(metadata.card_last_four) || !expiryMonth2(metadata.expiry_month) || !expiryYear2(metadata.expiry_year) || !FRAME_BRANDS.includes(metadata.card_brand))
726
875
  return null;
727
876
  const details = card.three_d_s;
728
- if (!record2(details) || !sameKeys(details, ["browser_info"]) || typeof details.browser_info !== "string" || !BROWSER_INFO.test(details.browser_info))
877
+ if (!record3(details) || !sameKeys(details, ["browser_info"]) || typeof details.browser_info !== "string" || !BROWSER_INFO.test(details.browser_info))
729
878
  return null;
730
879
  const tokenex = card.tokenex_card;
731
- if (!record2(tokenex) || !sameKeys(tokenex, CARD_KEYS))
880
+ if (!record3(tokenex) || !sameKeys(tokenex, CARD_KEYS))
732
881
  return null;
733
882
  for (const name of CARD_KEYS) {
734
883
  const blob = tokenex[name];
@@ -743,14 +892,14 @@ function readPay(body, line, environment) {
743
892
  return null;
744
893
  if (card.session_tracking !== void 0) {
745
894
  const tracking = card.session_tracking;
746
- const radar = record2(tracking) && sameKeys(tracking, ["stripe_radar"]) ? tracking.stripe_radar : null;
747
- if (!record2(radar) || !radarKeys(radar) || typeof radar.token !== "string" || !RADAR_TOKEN.test(radar.token) || typeof radar.country_code !== "string" || !COUNTRY_CODE.test(radar.country_code))
895
+ const radar = record3(tracking) && sameKeys(tracking, ["stripe_radar"]) ? tracking.stripe_radar : null;
896
+ if (!record3(radar) || !radarKeys(radar) || typeof radar.token !== "string" || !RADAR_TOKEN.test(radar.token) || typeof radar.country_code !== "string" || !COUNTRY_CODE.test(radar.country_code))
748
897
  return null;
749
898
  }
750
899
  return value;
751
900
  }
752
901
  function paddleTokenexRequestRefusal(request, environment) {
753
- if (!record2(request) || typeof request.method !== "string" || request.method.toUpperCase() !== "POST")
902
+ if (!record3(request) || typeof request.method !== "string" || request.method.toUpperCase() !== "POST")
754
903
  return "paddle_tokenex_pay_unsupported";
755
904
  const target = paddleTokenexPayTarget(request.url);
756
905
  if (!target || environment !== void 0 && target.environment !== environment)
@@ -758,7 +907,7 @@ function paddleTokenexRequestRefusal(request, environment) {
758
907
  return readPay(request.body, target.line, environment) === null ? "paddle_tokenex_pay_unsupported" : null;
759
908
  }
760
909
  function isPaddleTokenexCardRequest(request) {
761
- return record2(request) && paddleTokenexPayCarriesCard(request.body) && paddleTokenexRequestRefusal(request) === null;
910
+ return record3(request) && paddleTokenexPayCarriesCard(request.body) && paddleTokenexRequestRefusal(request) === null;
762
911
  }
763
912
  function paddleTokenexBodyVerdict(body, line) {
764
913
  if (!paddleTokenexPayCarriesCard(body))
@@ -767,7 +916,7 @@ function paddleTokenexBodyVerdict(body, line) {
767
916
  if (readPay(body, candidate) !== null)
768
917
  return CARD;
769
918
  }
770
- return refuse2("paddle_tokenex_pay_unsupported");
919
+ return refuse3("paddle_tokenex_pay_unsupported");
771
920
  }
772
921
  var REFUSAL_MESSAGES = /* @__PURE__ */ Object.freeze({
773
922
  paddle_tokenex_pay_unsupported: "Refusing to pay: this is not the card payment Paddle's checkout sends. Nothing was sent.",
@@ -824,7 +973,7 @@ function base64(bytes) {
824
973
  return btoa(binary);
825
974
  }
826
975
  function paddleTokenexSubstitutionOf(value, environment) {
827
- if (!record2(value))
976
+ if (!record3(value))
828
977
  return null;
829
978
  const keys = Object.keys(value);
830
979
  const all = [...PADDLE_TOKENEX_FIELDS, ...PADDLE_TOKENEX_FACT_KEYS];
@@ -881,7 +1030,7 @@ function substitutePaddleTokenex(url, body, substitution) {
881
1030
  return out;
882
1031
  }
883
1032
  function paddleTokenexKeyUsable(authorization) {
884
- if (!record2(authorization) || !record2(authorization.cse))
1033
+ if (!record3(authorization) || !record3(authorization.cse))
885
1034
  return false;
886
1035
  const cse = authorization.cse;
887
1036
  if (cse.psp !== PADDLE_TOKENEX_PSP || cse.scheme !== PADDLE_TOKENEX_SCHEME)
@@ -895,7 +1044,7 @@ function paddleTokenexKeyUsable(authorization) {
895
1044
  }
896
1045
  function paddleTokenexSubstitutionsRefusal(template, substitution) {
897
1046
  const refuseWith = (message) => ({ code: "invalid_substitutions", message });
898
- if (!record2(template))
1047
+ if (!record3(template))
899
1048
  return refuseWith("This authorization is not a Paddle TokenEx /pay.");
900
1049
  const target = paddleTokenexPayTarget(template.url);
901
1050
  if (!target)
@@ -920,7 +1069,7 @@ function readAnswer(text) {
920
1069
  return null;
921
1070
  try {
922
1071
  const value = JSON.parse(text);
923
- return record2(value) ? value : null;
1072
+ return record3(value) ? value : null;
924
1073
  }
925
1074
  catch {
926
1075
  return null;
@@ -931,12 +1080,12 @@ function paddleTokenexRefusalCode(text) {
931
1080
  const answer = readAnswer(text);
932
1081
  if (!answer)
933
1082
  return null;
934
- const first = Array.isArray(answer.errors) && record2(answer.errors[0]) ? answer.errors[0] : null;
1083
+ const first = Array.isArray(answer.errors) && record3(answer.errors[0]) ? answer.errors[0] : null;
935
1084
  return boundedCode(first && first.error_code) ?? boundedCode(first && first.code) ?? boundedCode(answer.error_code) ?? boundedCode(answer.code);
936
1085
  }
937
1086
  function needs3ds(answer) {
938
- const action = record2(answer) && record2(answer.data) ? answer.data.required_action : record2(answer) ? answer.required_action : null;
939
- return record2(action) && typeof action.type === "string" && /(^|_)card$/.test(action.type);
1087
+ const action = record3(answer) && record3(answer.data) ? answer.data.required_action : record3(answer) ? answer.required_action : null;
1088
+ return record3(action) && typeof action.type === "string" && /(^|_)card$/.test(action.type);
940
1089
  }
941
1090
  function paddleTokenexAnswerOutcome(status, text) {
942
1091
  if (!Number.isInteger(status))
@@ -952,4 +1101,4 @@ function paddleTokenexAnswerOutcome(status, text) {
952
1101
  return { outcome: "unknown", reason: "paddle_tokenex_unconfirmed" };
953
1102
  return needs3ds(answer) ? { outcome: "challenge" } : { outcome: "approved" };
954
1103
  }
955
- export { PADDLE_TOKENEX_CAPABILITY, PADDLE_TOKENEX_CARD_NETWORKS, PADDLE_TOKENEX_FACT_KEYS, PADDLE_TOKENEX_FIELDS, PADDLE_TOKENEX_OPERATION, PADDLE_TOKENEX_PSP, PADDLE_TOKENEX_REFUSAL_CODES, PADDLE_TOKENEX_SCHEME, encryptPaddleTokenexCard, isPaddleTokenexCardRequest, isPaddleTokenexCiphertext, isPaddleTokenexCiphertextUnderKey, paddleTokenexAnswerOutcome, paddleTokenexBodyVerdict, paddleVgsCardFactsAgree as paddleTokenexCardFactsAgree, paddleTokenexKeyUsable, paddleTokenexPayCarriesCard, paddleTokenexPayTarget, paddleTokenexRefusalCode, paddleTokenexRefusalMessage, paddleTokenexRequestRefusal, paddleTokenexStoredCardRefusal, paddleTokenexSubstitutionOf, paddleTokenexSubstitutionsRefusal, substitutePaddleTokenex };
1104
+ export { PADDLE_CHECKOUT_ANSWERS_MAX, PADDLE_CHECKOUT_ANSWER_LIMIT, PADDLE_CHECKOUT_TERMS_REFUSALS, PADDLE_TOKENEX_CAPABILITY, PADDLE_TOKENEX_CARD_NETWORKS, PADDLE_TOKENEX_FACT_KEYS, PADDLE_TOKENEX_FIELDS, PADDLE_TOKENEX_OPERATION, PADDLE_TOKENEX_PSP, PADDLE_TOKENEX_REFUSAL_CODES, PADDLE_TOKENEX_SCHEME, encryptPaddleTokenexCard, isPaddleTokenexCardRequest, isPaddleTokenexCiphertext, isPaddleTokenexCiphertextUnderKey, paddleCheckoutAnswerTarget, paddleCheckoutTerms, paddleMinorUnits, paddleTokenexAnswerOutcome, paddleTokenexBodyVerdict, paddleVgsCardFactsAgree as paddleTokenexCardFactsAgree, paddleTokenexKeyUsable, paddleTokenexPayCarriesCard, paddleTokenexPayTarget, paddleTokenexRefusalCode, paddleTokenexRefusalMessage, paddleTokenexRequestRefusal, paddleTokenexStoredCardRefusal, paddleTokenexSubstitutionOf, paddleTokenexSubstitutionsRefusal, projectPaddleCheckoutAnswer, substitutePaddleTokenex };
@@ -32,8 +32,13 @@
32
32
  * hipay: prepared only. The merchant's page gives HiPay's hosted fields 20 seconds after Pay, too soon for an
33
33
  * approval after Pay. Its environments are its two vault hosts: secure2-vault (production) and
34
34
  * stage-secure2-vault (sandbox).
35
+ *
36
+ * openpay_bbva: prepared only. openpay.js abandons its token create 40 seconds after Pay, too soon for an
37
+ * approval after Pay. Its environments are its API hosts: api.openpay.<mx|co|pe> (production) and
38
+ * sandbox-api.openpay.<mx|co|pe> (sandbox). The token URL names the merchant in its path, so no URL is
39
+ * prepared in advance: the claim binds the request by its host's environment and its shape, as ACI's.
35
40
  */
36
- export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv' | 'cybersource_flex' | 'aci' | 'creditguard' | 'worldpay_eprotect' | 'amadeus_checkout' | 'verifone' | 'cybersource_secure_acceptance' | 'paddle_vgs' | 'wix_payments' | 'paygent' | 'hipay';
41
+ export type PreparationProcessor = 'square' | 'braintree' | 'worldpay' | 'bambora' | 'mercado_pago' | 'recurly' | 'spreedly' | 'adyen' | 'checkout_com' | 'paysafe' | 'fiserv' | 'cybersource_flex' | 'aci' | 'creditguard' | 'worldpay_eprotect' | 'amadeus_checkout' | 'verifone' | 'cybersource_secure_acceptance' | 'paddle_vgs' | 'wix_payments' | 'paygent' | 'hipay' | 'openpay_bbva';
37
42
  export type PreparationEnvironment = 'production' | 'sandbox' | 'shared';
38
43
  /**
39
44
  * How the approved card reaches the processor: the device's own request (token), or
@@ -71,7 +76,8 @@ export declare function preparedApprovalAnswersRetry(psp: string): boolean;
71
76
  * profile pins. CyberSource Secure Acceptance's are separate hosts too: secureacceptance is
72
77
  * live, testsecureacceptance is test. So are Paddle VGS's two card URLs: vault is live,
73
78
  * sandbox-vault is test. So are Paygent's two token hosts: token is live, sandbox is test. So are
74
- * HiPay's two vault hosts: secure2-vault is live, stage-secure2-vault is test.
79
+ * HiPay's two vault hosts: secure2-vault is live, stage-secure2-vault is test. So are Openpay's API
80
+ * hosts: api.openpay.<mx|co|pe> is live, sandbox-api.openpay.<mx|co|pe> is test.
75
81
  */
76
82
  export declare function validPreparationEnvironment(psp: string, environment: string): boolean;
77
83
  /**
@@ -83,8 +89,10 @@ export declare function validPreparationEnvironment(psp: string, environment: st
83
89
  export declare function aciPreparationUrl(environment: PreparationEnvironment): string;
84
90
  /**
85
91
  * A processor whose card request names an id the merchant mints only at Pay time (ACI's checkout
86
- * id), so no URL can be prepared in advance: whether the adapters can intercept it is asked of the
87
- * registry (VaultClient.recognizesProcessor), not of the prepared endpoint.
92
+ * id), or the merchant's own id (Openpay's token URL carries the merchant id in its path and the
93
+ * country in its host), so no URL can be prepared in advance: whether the adapters can intercept it
94
+ * is asked of the registry (VaultClient.recognizesProcessor), not of the prepared endpoint, which
95
+ * only names the preparation.
88
96
  */
89
97
  export declare function preparationBindsByShape(psp: string): boolean;
90
98
  /**