@asteby/storefront-react 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ import { StorefrontClient, CheckoutResult, Cart, PaymentAttemptResult } from '@asteby/storefront';
2
+ import { P as PayChoice } from './choices-DEPE9KRq.js';
3
+ export { B as BuildPayChoicesOptions, a as PayChoices, b as PayErrorBody, c as PaymentCspMode, d as PaymentCspOptions, V as VoucherRowLabels, e as buildPayChoices, f as cashProviderData, g as checkoutToAttempt, h as choiceValue, p as parseChoice, i as payErrorResponse, j as paymentCsp, k as paymentCspHeaderName, l as paymentCspHeaders, m as paymentCspMode, n as paymentNonce, o as paymentNonceFromHeaders, v as voucherRows } from './choices-DEPE9KRq.js';
4
+
5
+ /**
6
+ * Server side of the embedded payment (`@asteby/storefront-react/payments/server`): the two JSON resource routes the
7
+ * browser talks to (charge the card, re-read the payment) and the cash voucher of the page form. No React, no router:
8
+ * Web `Request`/`Response` only, so any framework mounts it.
9
+ *
10
+ * The browser NEVER calls OPS. These handlers translate to `carts.checkout` + `checkouts.pay` with the shop's `sk_` token.
11
+ *
12
+ * Money: no amount is computed here. `expected_total` is the total the server showed the buyer (echoed back by the SDK);
13
+ * OPS compares it with the order and answers `422 price_mismatch` when it changed.
14
+ *
15
+ * Everything shop-specific (cookies, receipt shape, which fields the form requires, how contact/delivery are saved)
16
+ * arrives through `EmbeddedPayConfig` callbacks.
17
+ */
18
+
19
+ type RateLimiter = {
20
+ hit(key: string, now?: number): {
21
+ allowed: boolean;
22
+ retryAfterSec: number;
23
+ };
24
+ };
25
+ /** In-memory sliding window per key. Protects this process; OPS puts its own cap per checkout on top. */
26
+ declare function createRateLimiter({ limit, windowMs }: {
27
+ limit: number;
28
+ windowMs: number;
29
+ }): RateLimiter;
30
+ /** Buyer IP as forwarded by a trusted proxy/CDN; without `trustProxy` a single shared key. */
31
+ declare function clientKey(request: Request, trustProxy?: boolean): string;
32
+ /**
33
+ * A request from another site cannot pay with the buyer's cookie. Only the HOST is compared (no scheme or port): behind
34
+ * the proxy the request scheme can be http while the browser sent https.
35
+ */
36
+ declare function sameOrigin(request: Request, site: string): boolean;
37
+ /** Device fingerprint (anti-fraud): OPTIONAL, so it never breaks a payment. Anything over 200 chars or odd is dropped. */
38
+ declare const DEVICE_SESSION_ID_MAX = 200;
39
+ declare function deviceSessionId(v: unknown): string | undefined;
40
+ /** What the shop keeps between the checkout creation and the end of the payment (usually in a cookie). */
41
+ type PaySession<TDraft extends {
42
+ checkout_id?: string;
43
+ }> = {
44
+ cart_id: string;
45
+ draft: TDraft;
46
+ };
47
+ type EmbeddedPayMessages = {
48
+ /** Required form fields missing. */
49
+ fillFields: string;
50
+ /** The cart cookie is gone or the checkout expired. */
51
+ expiredCart: string;
52
+ /** The chosen row is not payable in the page (the server decides, not the browser). */
53
+ methodUnavailable: string;
54
+ tooManyRequests: string;
55
+ cashChoosePoint: string;
56
+ };
57
+ type EmbeddedPayConfig<TDraft extends {
58
+ checkout_id?: string;
59
+ }> = {
60
+ /** The store client with the `sk_` token for this request. */
61
+ client: (request: Request) => Promise<StorefrontClient>;
62
+ /** Public origin of the shop (`https://tienda.com`). Used for `return_url` and the origin check. */
63
+ site: (request: Request) => string;
64
+ /** Path of the thank-you page, appended to `site`. */
65
+ returnPath: string;
66
+ /** Locale for the outcome texts and voucher dates. Default `es-MX`. */
67
+ locale?: string;
68
+ readCartId: (request: Request) => Promise<string | null>;
69
+ readSession: (request: Request) => Promise<PaySession<TDraft> | null>;
70
+ /** `Set-Cookie` value that keeps the session so a retry reuses the already created checkout. */
71
+ serializeSession: (session: PaySession<TDraft>) => Promise<string>;
72
+ /** `null` when the form is complete; otherwise the message to show (HTTP 400). The shop knows its required fields. */
73
+ validateFields: (fields: Record<string, string>) => string | null;
74
+ /** Saves contact, address and delivery on the server cart (the cart becomes an order at checkout, so do it before). */
75
+ prepareCart: (args: {
76
+ sf: StorefrontClient;
77
+ cartId: string;
78
+ fields: Record<string, string>;
79
+ }) => Promise<void>;
80
+ /** First draft of the receipt from the created checkout and the cart as it was before. */
81
+ createDraft: (args: {
82
+ created: CheckoutResult;
83
+ before: Cart | null;
84
+ fields: Record<string, string>;
85
+ }) => TDraft;
86
+ /**
87
+ * Called after every attempt. When the order is closed (approved, under review, voucher) append the receipt cookie and
88
+ * clear the cart/session cookies to `headers` and return `true`; otherwise return `false` (the session is kept).
89
+ */
90
+ settle: (args: {
91
+ draft: TDraft;
92
+ result: PaymentAttemptResult;
93
+ headers: Headers;
94
+ }) => Promise<boolean>;
95
+ /** Method keys the shop never offers (the server also checks them: a forged `payment_choice` is refused). */
96
+ hide?: Iterable<string>;
97
+ messages?: Partial<EmbeddedPayMessages>;
98
+ /** Per-IP limit on charges. Default 10 per 10 minutes; `false` disables (OPS still caps attempts per checkout). */
99
+ rateLimit?: RateLimiter | {
100
+ limit?: number;
101
+ windowMs?: number;
102
+ } | false;
103
+ /** Trust `cf-connecting-ip` / `x-forwarded-for` for the limiter key. */
104
+ trustProxy?: boolean;
105
+ /** Cache header of every response. Default `private, no-store`. */
106
+ cacheControl?: string;
107
+ /** Diagnostic log of an OPS rejection (no bodies, no buyer data). Default: one JSON line with `console.warn`. */
108
+ log?: (entry: {
109
+ level: 'warn';
110
+ msg: string;
111
+ status: number;
112
+ code: string | null;
113
+ field: string | null;
114
+ message: string;
115
+ }) => void;
116
+ };
117
+ type CashOutcome = {
118
+ kind: 'done';
119
+ headers: Headers;
120
+ } | {
121
+ kind: 'error';
122
+ status: number;
123
+ message: string;
124
+ fields: string[];
125
+ };
126
+ type EmbeddedPayHandlers = {
127
+ /** POST JSON from the card form: creates the checkout (idempotent per cart) and charges the attempt. */
128
+ pay: (request: Request) => Promise<Response>;
129
+ /** POST: re-reads the checkout (3DS challenge, "under review") in the shape of an attempt. */
130
+ status: (request: Request) => Promise<Response>;
131
+ /** Cash from the page form (with or without JavaScript): creates the `embedded/cash` checkout and asks for the voucher. */
132
+ cash: (args: {
133
+ sf: StorefrontClient;
134
+ request: Request;
135
+ cartId: string;
136
+ fields: Record<string, string>;
137
+ choice: PayChoice;
138
+ before: Cart | null;
139
+ }) => Promise<CashOutcome>;
140
+ };
141
+ declare function createEmbeddedPayHandlers<TDraft extends {
142
+ checkout_id?: string;
143
+ }>(config: EmbeddedPayConfig<TDraft>): EmbeddedPayHandlers;
144
+
145
+ export { type CashOutcome, DEVICE_SESSION_ID_MAX, type EmbeddedPayConfig, type EmbeddedPayHandlers, type EmbeddedPayMessages, PayChoice, type PaySession, type RateLimiter, clientKey, createEmbeddedPayHandlers, createRateLimiter, deviceSessionId, sameOrigin };
@@ -0,0 +1,191 @@
1
+ import { parseChoice, buildPayChoices, payErrorResponse, checkoutToAttempt, cashProviderData } from './chunk-ISEOCKIR.js';
2
+ export { buildPayChoices, cashProviderData, checkoutToAttempt, choiceValue, parseChoice, payErrorResponse, paymentCsp, paymentCspHeaderName, paymentCspHeaders, paymentCspMode, paymentNonce, paymentNonceFromHeaders, voucherRows } from './chunk-ISEOCKIR.js';
3
+ import { paymentIdempotencyKey, isStorefrontError, paymentOutcome, safeHttpsUrl } from '@asteby/storefront';
4
+
5
+ function createRateLimiter({ limit, windowMs }) {
6
+ const hits = /* @__PURE__ */ new Map();
7
+ return {
8
+ hit(key, now = Date.now()) {
9
+ const recent = (hits.get(key) ?? []).filter((t) => now - t < windowMs);
10
+ if (recent.length >= limit) {
11
+ hits.set(key, recent);
12
+ return { allowed: false, retryAfterSec: Math.max(1, Math.ceil((recent[0] + windowMs - now) / 1e3)) };
13
+ }
14
+ recent.push(now);
15
+ hits.set(key, recent);
16
+ if (hits.size > 5e3) {
17
+ for (const [k, v] of hits) if (v.every((t) => now - t >= windowMs)) hits.delete(k);
18
+ }
19
+ return { allowed: true, retryAfterSec: 0 };
20
+ }
21
+ };
22
+ }
23
+ function clientKey(request, trustProxy = false) {
24
+ if (!trustProxy) return "direct";
25
+ const cf = request.headers.get("cf-connecting-ip");
26
+ if (cf) return cf.trim();
27
+ return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "direct";
28
+ }
29
+ function sameOrigin(request, site) {
30
+ const origin = request.headers.get("origin");
31
+ if (!origin) return true;
32
+ try {
33
+ const host = new URL(origin).hostname;
34
+ return host === new URL(site).hostname || host === new URL(request.url).hostname;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ var DEVICE_SESSION_ID_MAX = 200;
40
+ function deviceSessionId(v) {
41
+ if (typeof v !== "string") return void 0;
42
+ const id = v.trim();
43
+ return id.length > 0 && id.length <= DEVICE_SESSION_ID_MAX && /^[\w.:-]+$/.test(id) ? id : void 0;
44
+ }
45
+ var ATTEMPT = /^[A-Za-z0-9_-]{8,100}$/;
46
+ var asMoney = (v) => {
47
+ const m = v;
48
+ return m && typeof m.amount === "string" && /^\d+(\.\d+)?$/.test(m.amount) && typeof m.currency_code === "string" && m.currency_code.length === 3 ? { amount: m.amount, currency_code: m.currency_code } : null;
49
+ };
50
+ var DEFAULT_MESSAGES = {
51
+ fillFields: "Completa los campos marcados.",
52
+ expiredCart: "Tu carrito venci\xF3. Vuelve a armarlo para pagar.",
53
+ methodUnavailable: "Esta forma de pago no est\xE1 disponible por ahora.",
54
+ tooManyRequests: "Demasiados intentos. Espera unos minutos e int\xE9ntalo de nuevo.",
55
+ cashChoosePoint: "Elige d\xF3nde pagar en efectivo."
56
+ };
57
+ function createEmbeddedPayHandlers(config) {
58
+ const msg = { ...DEFAULT_MESSAGES, ...config.messages };
59
+ const locale = config.locale ?? "es-MX";
60
+ const cache = config.cacheControl ?? "private, no-store";
61
+ const makeLimiter = () => {
62
+ const r = config.rateLimit;
63
+ if (r === false) return null;
64
+ if (r && "hit" in r) return r;
65
+ return createRateLimiter({ limit: Math.max(1, r?.limit ?? 10), windowMs: r?.windowMs ?? 10 * 6e4 });
66
+ };
67
+ const limiter = makeLimiter();
68
+ const log = config.log ?? ((entry) => {
69
+ console.warn(JSON.stringify(entry));
70
+ });
71
+ const json = (body, init = {}) => {
72
+ const headers = new Headers(init.headers);
73
+ headers.set("Cache-Control", cache);
74
+ headers.set("Content-Type", "application/json");
75
+ return new Response(JSON.stringify(body), { status: init.status ?? 200, headers });
76
+ };
77
+ const fail = (status2, code, message, extra = {}) => json({ error: { code, message } }, { status: status2, headers: extra });
78
+ async function pay(request) {
79
+ const site = config.site(request);
80
+ if (!sameOrigin(request, site)) return fail(403, "forbidden", "origin");
81
+ if (limiter) {
82
+ const gate = limiter.hit(clientKey(request, config.trustProxy));
83
+ if (!gate.allowed) return fail(429, "too_many_attempts", msg.tooManyRequests, { "Retry-After": String(gate.retryAfterSec) });
84
+ }
85
+ const cartId = await config.readCartId(request);
86
+ if (!cartId) return fail(409, "expired", msg.expiredCart);
87
+ let body;
88
+ try {
89
+ body = await request.json();
90
+ } catch {
91
+ return fail(400, "validation", msg.fillFields);
92
+ }
93
+ const attempt = typeof body.attempt === "string" && ATTEMPT.test(body.attempt) ? body.attempt : null;
94
+ const expected = asMoney(body.expected_total);
95
+ const data = body.provider_data;
96
+ if (body.option !== "card" || !attempt || !expected || !data || typeof data !== "object" || Array.isArray(data)) {
97
+ return fail(400, "validation", msg.fillFields);
98
+ }
99
+ const { transaction_amount: _ignored, ...providerData } = data;
100
+ const fields = {};
101
+ for (const [k, v] of Object.entries(body.fields ?? {})) if (typeof v === "string") fields[k] = v.trim();
102
+ const invalid = config.validateFields(fields);
103
+ if (invalid) return fail(400, "validation", invalid);
104
+ const sf = await config.client(request);
105
+ const choice = parseChoice(fields.payment_choice);
106
+ let session;
107
+ try {
108
+ const methods = await sf.carts.paymentMethods(cartId);
109
+ const card = buildPayChoices(methods.methods, config.hide ? { hide: config.hide } : {}).choices.find((c) => c.mode === "card" && c.value === fields.payment_choice);
110
+ if (!choice || !card) return fail(422, "invalid_state", msg.methodUnavailable);
111
+ const previous = await config.readSession(request);
112
+ const reusedId = previous && previous.cart_id === cartId ? previous.draft.checkout_id : void 0;
113
+ let checkoutId;
114
+ if (previous && reusedId) {
115
+ session = previous;
116
+ checkoutId = reusedId;
117
+ } else {
118
+ await config.prepareCart({ sf, cartId, fields });
119
+ const before = await sf.carts.get(cartId).catch(() => null);
120
+ const created = await sf.carts.checkout(
121
+ cartId,
122
+ { payment_method: card.methodKey, payment_flow: "embedded", payment_option: "card", expected_total: expected, return_url: `${site}${config.returnPath}` },
123
+ { idempotencyKey: `chk-${cartId}` }
124
+ );
125
+ checkoutId = created.checkout.id;
126
+ session = { cart_id: cartId, draft: config.createDraft({ created, before, fields }) };
127
+ }
128
+ const device = deviceSessionId(body.device_session_id);
129
+ const result = await sf.checkouts.pay(
130
+ checkoutId,
131
+ { method: "card", provider_data: providerData, ...device ? { device_session_id: device } : {}, expected_total: expected },
132
+ { idempotencyKey: paymentIdempotencyKey(checkoutId, attempt) }
133
+ );
134
+ const headers = new Headers();
135
+ const closed = await config.settle({ draft: session.draft, result, headers });
136
+ if (!closed) headers.append("Set-Cookie", await config.serializeSession(session));
137
+ return json(result, { headers });
138
+ } catch (err) {
139
+ if (isStorefrontError(err)) {
140
+ const { status: status2, body: errBody } = payErrorResponse(err);
141
+ log({ level: "warn", msg: "cobro rechazado por OPS", status: err.status, code: err.code ?? null, field: err.field ?? null, message: String(err.message).slice(0, 300) });
142
+ const headers = new Headers();
143
+ if (session) headers.append("Set-Cookie", await config.serializeSession(session));
144
+ return json(errBody, { status: status2, headers });
145
+ }
146
+ throw err;
147
+ }
148
+ }
149
+ async function status(request) {
150
+ if (!sameOrigin(request, config.site(request))) return fail(403, "forbidden", "origin");
151
+ const session = await config.readSession(request);
152
+ const id = session?.draft.checkout_id;
153
+ if (!session || !id) return fail(404, "not_found", msg.expiredCart);
154
+ try {
155
+ const sf = await config.client(request);
156
+ const found = await sf.checkouts.get(id);
157
+ if (!found) return fail(404, "not_found", msg.expiredCart);
158
+ const attempt = checkoutToAttempt(found);
159
+ const headers = new Headers();
160
+ await config.settle({ draft: session.draft, result: attempt, headers });
161
+ return json(attempt, { headers });
162
+ } catch (err) {
163
+ if (isStorefrontError(err)) {
164
+ const { status: code, body } = payErrorResponse(err);
165
+ return json(body, { status: code });
166
+ }
167
+ throw err;
168
+ }
169
+ }
170
+ async function cash(args) {
171
+ const { sf, request, cartId, fields, choice, before } = args;
172
+ const providerData = cashProviderData(choice.points, fields.cash_point);
173
+ if (!providerData) return { kind: "error", status: 422, message: msg.cashChoosePoint, fields: ["cash_point"] };
174
+ const created = await sf.carts.checkout(
175
+ cartId,
176
+ { payment_method: choice.methodKey, payment_flow: "embedded", payment_option: "cash", return_url: `${config.site(request)}${config.returnPath}` },
177
+ { idempotencyKey: `chk-${cartId}-cash` }
178
+ );
179
+ const draft = config.createDraft({ created, before, fields });
180
+ const result = await sf.checkouts.pay(created.checkout.id, { method: "cash", provider_data: providerData }, { idempotencyKey: paymentIdempotencyKey(created.checkout.id, "cash") });
181
+ const headers = new Headers();
182
+ if (await config.settle({ draft, result, headers })) return { kind: "done", headers };
183
+ const outcome = paymentOutcome(result, locale);
184
+ return { kind: "error", status: safeHttpsUrl(outcome.redirect_url) ? 409 : 422, message: outcome.message, fields: [] };
185
+ }
186
+ return { pay, status, cash };
187
+ }
188
+
189
+ export { DEVICE_SESSION_ID_MAX, clientKey, createEmbeddedPayHandlers, createRateLimiter, deviceSessionId, sameOrigin };
190
+ //# sourceMappingURL=payments-server.js.map
191
+ //# sourceMappingURL=payments-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/payments/server.ts"],"names":["status"],"mappings":"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","file":"payments-server.js","sourcesContent":["/**\n * Server side of the embedded payment (`@asteby/storefront-react/payments/server`): the two JSON resource routes the\n * browser talks to (charge the card, re-read the payment) and the cash voucher of the page form. No React, no router:\n * Web `Request`/`Response` only, so any framework mounts it.\n *\n * The browser NEVER calls OPS. These handlers translate to `carts.checkout` + `checkouts.pay` with the shop's `sk_` token.\n *\n * Money: no amount is computed here. `expected_total` is the total the server showed the buyer (echoed back by the SDK);\n * OPS compares it with the order and answers `422 price_mismatch` when it changed.\n *\n * Everything shop-specific (cookies, receipt shape, which fields the form requires, how contact/delivery are saved)\n * arrives through `EmbeddedPayConfig` callbacks.\n */\nimport {\n isStorefrontError,\n paymentIdempotencyKey,\n paymentOutcome,\n safeHttpsUrl,\n type Cart,\n type CheckoutResult,\n type Money,\n type PaymentAttemptResult,\n type StorefrontClient,\n} from '@asteby/storefront'\nimport { buildPayChoices, cashProviderData, checkoutToAttempt, parseChoice, payErrorResponse, type PayChoice } from './choices.js'\n\nexport * from './choices.js'\nexport { paymentCsp, paymentCspHeaderName, paymentCspHeaders, paymentCspMode, paymentNonce, paymentNonceFromHeaders } from './csp.js'\nexport type { PaymentCspMode, PaymentCspOptions } from './csp.js'\n\nexport type RateLimiter = { hit(key: string, now?: number): { allowed: boolean; retryAfterSec: number } }\n\n/** In-memory sliding window per key. Protects this process; OPS puts its own cap per checkout on top. */\nexport function createRateLimiter({ limit, windowMs }: { limit: number; windowMs: number }): RateLimiter {\n const hits = new Map<string, number[]>()\n return {\n hit(key, now = Date.now()) {\n const recent = (hits.get(key) ?? []).filter((t) => now - t < windowMs)\n if (recent.length >= limit) {\n hits.set(key, recent)\n return { allowed: false, retryAfterSec: Math.max(1, Math.ceil((recent[0]! + windowMs - now) / 1000)) }\n }\n recent.push(now)\n hits.set(key, recent)\n if (hits.size > 5000) for (const [k, v] of hits) if (v.every((t) => now - t >= windowMs)) hits.delete(k)\n return { allowed: true, retryAfterSec: 0 }\n },\n }\n}\n\n/** Buyer IP as forwarded by a trusted proxy/CDN; without `trustProxy` a single shared key. */\nexport function clientKey(request: Request, trustProxy = false): string {\n if (!trustProxy) return 'direct'\n const cf = request.headers.get('cf-connecting-ip')\n if (cf) return cf.trim()\n return request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'direct'\n}\n\n/**\n * A request from another site cannot pay with the buyer's cookie. Only the HOST is compared (no scheme or port): behind\n * the proxy the request scheme can be http while the browser sent https.\n */\nexport function sameOrigin(request: Request, site: string): boolean {\n const origin = request.headers.get('origin')\n if (!origin) return true\n try {\n const host = new URL(origin).hostname\n return host === new URL(site).hostname || host === new URL(request.url).hostname\n } catch {\n return false\n }\n}\n\n/** Device fingerprint (anti-fraud): OPTIONAL, so it never breaks a payment. Anything over 200 chars or odd is dropped. */\nexport const DEVICE_SESSION_ID_MAX = 200\nexport function deviceSessionId(v: unknown): string | undefined {\n if (typeof v !== 'string') return undefined\n const id = v.trim()\n return id.length > 0 && id.length <= DEVICE_SESSION_ID_MAX && /^[\\w.:-]+$/.test(id) ? id : undefined\n}\n\nconst ATTEMPT = /^[A-Za-z0-9_-]{8,100}$/\n\nconst asMoney = (v: unknown): Money | null => {\n const m = v as Partial<Money> | null\n return m && typeof m.amount === 'string' && /^\\d+(\\.\\d+)?$/.test(m.amount) && typeof m.currency_code === 'string' && m.currency_code.length === 3\n ? { amount: m.amount, currency_code: m.currency_code }\n : null\n}\n\n/** What the shop keeps between the checkout creation and the end of the payment (usually in a cookie). */\nexport type PaySession<TDraft extends { checkout_id?: string }> = { cart_id: string; draft: TDraft }\n\nexport type EmbeddedPayMessages = {\n /** Required form fields missing. */\n fillFields: string\n /** The cart cookie is gone or the checkout expired. */\n expiredCart: string\n /** The chosen row is not payable in the page (the server decides, not the browser). */\n methodUnavailable: string\n tooManyRequests: string\n cashChoosePoint: string\n}\n\nconst DEFAULT_MESSAGES: EmbeddedPayMessages = {\n fillFields: 'Completa los campos marcados.',\n expiredCart: 'Tu carrito venció. Vuelve a armarlo para pagar.',\n methodUnavailable: 'Esta forma de pago no está disponible por ahora.',\n tooManyRequests: 'Demasiados intentos. Espera unos minutos e inténtalo de nuevo.',\n cashChoosePoint: 'Elige dónde pagar en efectivo.',\n}\n\nexport type EmbeddedPayConfig<TDraft extends { checkout_id?: string }> = {\n /** The store client with the `sk_` token for this request. */\n client: (request: Request) => Promise<StorefrontClient>\n /** Public origin of the shop (`https://tienda.com`). Used for `return_url` and the origin check. */\n site: (request: Request) => string\n /** Path of the thank-you page, appended to `site`. */\n returnPath: string\n /** Locale for the outcome texts and voucher dates. Default `es-MX`. */\n locale?: string\n readCartId: (request: Request) => Promise<string | null>\n readSession: (request: Request) => Promise<PaySession<TDraft> | null>\n /** `Set-Cookie` value that keeps the session so a retry reuses the already created checkout. */\n serializeSession: (session: PaySession<TDraft>) => Promise<string>\n /** `null` when the form is complete; otherwise the message to show (HTTP 400). The shop knows its required fields. */\n validateFields: (fields: Record<string, string>) => string | null\n /** Saves contact, address and delivery on the server cart (the cart becomes an order at checkout, so do it before). */\n prepareCart: (args: { sf: StorefrontClient; cartId: string; fields: Record<string, string> }) => Promise<void>\n /** First draft of the receipt from the created checkout and the cart as it was before. */\n createDraft: (args: { created: CheckoutResult; before: Cart | null; fields: Record<string, string> }) => TDraft\n /**\n * Called after every attempt. When the order is closed (approved, under review, voucher) append the receipt cookie and\n * clear the cart/session cookies to `headers` and return `true`; otherwise return `false` (the session is kept).\n */\n settle: (args: { draft: TDraft; result: PaymentAttemptResult; headers: Headers }) => Promise<boolean>\n /** Method keys the shop never offers (the server also checks them: a forged `payment_choice` is refused). */\n hide?: Iterable<string>\n messages?: Partial<EmbeddedPayMessages>\n /** Per-IP limit on charges. Default 10 per 10 minutes; `false` disables (OPS still caps attempts per checkout). */\n rateLimit?: RateLimiter | { limit?: number; windowMs?: number } | false\n /** Trust `cf-connecting-ip` / `x-forwarded-for` for the limiter key. */\n trustProxy?: boolean\n /** Cache header of every response. Default `private, no-store`. */\n cacheControl?: string\n /** Diagnostic log of an OPS rejection (no bodies, no buyer data). Default: one JSON line with `console.warn`. */\n log?: (entry: { level: 'warn'; msg: string; status: number; code: string | null; field: string | null; message: string }) => void\n}\n\nexport type CashOutcome = { kind: 'done'; headers: Headers } | { kind: 'error'; status: number; message: string; fields: string[] }\n\nexport type EmbeddedPayHandlers = {\n /** POST JSON from the card form: creates the checkout (idempotent per cart) and charges the attempt. */\n pay: (request: Request) => Promise<Response>\n /** POST: re-reads the checkout (3DS challenge, \"under review\") in the shape of an attempt. */\n status: (request: Request) => Promise<Response>\n /** Cash from the page form (with or without JavaScript): creates the `embedded/cash` checkout and asks for the voucher. */\n cash: (args: { sf: StorefrontClient; request: Request; cartId: string; fields: Record<string, string>; choice: PayChoice; before: Cart | null }) => Promise<CashOutcome>\n}\n\nexport function createEmbeddedPayHandlers<TDraft extends { checkout_id?: string }>(config: EmbeddedPayConfig<TDraft>): EmbeddedPayHandlers {\n const msg = { ...DEFAULT_MESSAGES, ...config.messages }\n const locale = config.locale ?? 'es-MX'\n const cache = config.cacheControl ?? 'private, no-store'\n const makeLimiter = (): RateLimiter | null => {\n const r = config.rateLimit\n if (r === false) return null\n if (r && 'hit' in r) return r\n return createRateLimiter({ limit: Math.max(1, r?.limit ?? 10), windowMs: r?.windowMs ?? 10 * 60_000 })\n }\n const limiter = makeLimiter()\n const log =\n config.log ??\n ((entry) => {\n console.warn(JSON.stringify(entry))\n })\n\n const json = (body: unknown, init: { status?: number; headers?: Headers | Record<string, string> } = {}) => {\n const headers = new Headers(init.headers)\n headers.set('Cache-Control', cache)\n headers.set('Content-Type', 'application/json')\n return new Response(JSON.stringify(body), { status: init.status ?? 200, headers })\n }\n const fail = (status: number, code: string, message: string, extra: Headers | Record<string, string> = {}) => json({ error: { code, message } }, { status, headers: extra })\n\n async function pay(request: Request): Promise<Response> {\n const site = config.site(request)\n if (!sameOrigin(request, site)) return fail(403, 'forbidden', 'origin')\n if (limiter) {\n const gate = limiter.hit(clientKey(request, config.trustProxy))\n if (!gate.allowed) return fail(429, 'too_many_attempts', msg.tooManyRequests, { 'Retry-After': String(gate.retryAfterSec) })\n }\n const cartId = await config.readCartId(request)\n if (!cartId) return fail(409, 'expired', msg.expiredCart)\n\n let body: { option?: unknown; attempt?: unknown; provider_data?: unknown; device_session_id?: unknown; expected_total?: unknown; fields?: unknown }\n try {\n body = (await request.json()) as typeof body\n } catch {\n return fail(400, 'validation', msg.fillFields)\n }\n const attempt = typeof body.attempt === 'string' && ATTEMPT.test(body.attempt) ? body.attempt : null\n const expected = asMoney(body.expected_total)\n const data = body.provider_data\n if (body.option !== 'card' || !attempt || !expected || !data || typeof data !== 'object' || Array.isArray(data)) {\n return fail(400, 'validation', msg.fillFields)\n }\n // The amount does not travel in `provider_data`: OPS sets it. Even if the browser (or a stray script) sends one, it is not forwarded.\n const { transaction_amount: _ignored, ...providerData } = data as Record<string, unknown>\n const fields: Record<string, string> = {}\n for (const [k, v] of Object.entries((body.fields ?? {}) as Record<string, unknown>)) if (typeof v === 'string') fields[k] = v.trim()\n const invalid = config.validateFields(fields)\n if (invalid) return fail(400, 'validation', invalid)\n\n const sf = await config.client(request)\n const choice = parseChoice(fields.payment_choice)\n let session: PaySession<TDraft> | undefined\n try {\n // The server decides what can be paid in the page: the browser only picks among what OPS published.\n const methods = await sf.carts.paymentMethods(cartId)\n const card = buildPayChoices(methods.methods, config.hide ? { hide: config.hide } : {}).choices.find((c) => c.mode === 'card' && c.value === fields.payment_choice)\n if (!choice || !card) return fail(422, 'invalid_state', msg.methodUnavailable)\n\n // A retry (the first charge failed or the network dropped) reuses the checkout already created: the cart is an\n // order by now and cannot be edited again. Only the attempt (the payment key) changes.\n const previous = await config.readSession(request)\n const reusedId = previous && previous.cart_id === cartId ? previous.draft.checkout_id : undefined\n let checkoutId: string\n if (previous && reusedId) {\n session = previous\n checkoutId = reusedId\n } else {\n await config.prepareCart({ sf, cartId, fields })\n const before = await sf.carts.get(cartId).catch(() => null)\n const created = await sf.carts.checkout(\n cartId,\n { payment_method: card.methodKey, payment_flow: 'embedded', payment_option: 'card', expected_total: expected, return_url: `${site}${config.returnPath}` },\n { idempotencyKey: `chk-${cartId}` },\n )\n checkoutId = created.checkout.id\n session = { cart_id: cartId, draft: config.createDraft({ created, before, fields }) }\n }\n\n const device = deviceSessionId(body.device_session_id)\n const result = await sf.checkouts.pay(\n checkoutId,\n { method: 'card', provider_data: providerData, ...(device ? { device_session_id: device } : {}), expected_total: expected },\n { idempotencyKey: paymentIdempotencyKey(checkoutId, attempt) },\n )\n\n const headers = new Headers()\n const closed = await config.settle({ draft: session.draft, result, headers })\n if (!closed) headers.append('Set-Cookie', await config.serializeSession(session))\n return json(result, { headers })\n } catch (err) {\n if (isStorefrontError(err)) {\n const { status, body: errBody } = payErrorResponse(err)\n log({ level: 'warn', msg: 'cobro rechazado por OPS', status: err.status, code: err.code ?? null, field: err.field ?? null, message: String(err.message).slice(0, 300) })\n // 5xx, network or timeout: the charge may have happened; the browser retries with the SAME key.\n // The checkout exists even when the charge fails: kept so the retry reuses it.\n const headers = new Headers()\n if (session) headers.append('Set-Cookie', await config.serializeSession(session))\n return json(errBody, { status, headers })\n }\n throw err\n }\n }\n\n async function status(request: Request): Promise<Response> {\n if (!sameOrigin(request, config.site(request))) return fail(403, 'forbidden', 'origin')\n const session = await config.readSession(request)\n const id = session?.draft.checkout_id\n if (!session || !id) return fail(404, 'not_found', msg.expiredCart)\n try {\n const sf = await config.client(request)\n const found = await sf.checkouts.get(id)\n if (!found) return fail(404, 'not_found', msg.expiredCart)\n const attempt = checkoutToAttempt(found)\n const headers = new Headers()\n await config.settle({ draft: session.draft, result: attempt, headers })\n return json(attempt, { headers })\n } catch (err) {\n if (isStorefrontError(err)) {\n const { status: code, body } = payErrorResponse(err)\n return json(body, { status: code })\n }\n throw err\n }\n }\n\n async function cash(args: Parameters<EmbeddedPayHandlers['cash']>[0]): Promise<CashOutcome> {\n const { sf, request, cartId, fields, choice, before } = args\n const providerData = cashProviderData(choice.points, fields.cash_point)\n if (!providerData) return { kind: 'error', status: 422, message: msg.cashChoosePoint, fields: ['cash_point'] }\n const created = await sf.carts.checkout(\n cartId,\n { payment_method: choice.methodKey, payment_flow: 'embedded', payment_option: 'cash', return_url: `${config.site(request)}${config.returnPath}` },\n { idempotencyKey: `chk-${cartId}-cash` },\n )\n const draft = config.createDraft({ created, before, fields })\n // One fixed key per checkout: a double submit returns the SAME voucher.\n const result = await sf.checkouts.pay(created.checkout.id, { method: 'cash', provider_data: providerData }, { idempotencyKey: paymentIdempotencyKey(created.checkout.id, 'cash') })\n const headers = new Headers()\n if (await config.settle({ draft, result, headers })) return { kind: 'done', headers }\n const outcome = paymentOutcome(result, locale)\n return { kind: 'error', status: safeHttpsUrl(outcome.redirect_url) ? 409 : 422, message: outcome.message, fields: [] }\n }\n\n return { pay, status, cash }\n}\n"]}