@asteby/storefront-react 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -1
- package/dist/checkout.cjs +54 -17
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +8 -1
- package/dist/checkout.d.ts +8 -1
- package/dist/checkout.js +53 -18
- package/dist/checkout.js.map +1 -1
- package/dist/{chunk-ISEOCKIR.js → chunk-MNLROALF.js} +96 -8
- package/dist/chunk-MNLROALF.js.map +1 -0
- package/dist/{choices-DEPE9KRq.d.cts → csp-DZPuExzu.d.cts} +93 -55
- package/dist/{choices-DEPE9KRq.d.ts → csp-DZPuExzu.d.ts} +93 -55
- package/dist/payments-server.cjs +159 -12
- package/dist/payments-server.cjs.map +1 -1
- package/dist/payments-server.d.cts +23 -3
- package/dist/payments-server.d.ts +23 -3
- package/dist/payments-server.js +70 -13
- package/dist/payments-server.js.map +1 -1
- package/dist/payments.cjs +722 -150
- package/dist/payments.cjs.map +1 -1
- package/dist/payments.css +19 -0
- package/dist/payments.d.cts +207 -6
- package/dist/payments.d.ts +207 -6
- package/dist/payments.js +566 -92
- package/dist/payments.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-ISEOCKIR.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StorefrontClient, CheckoutResult, Cart, PaymentAttemptResult } from '@asteby/storefront';
|
|
2
|
-
import { P as PayChoice } from './
|
|
3
|
-
export { B as BuildPayChoicesOptions, a as PayChoices, b as PayErrorBody,
|
|
2
|
+
import { h as PaymentRecoveryMessages, P as PayChoice, c as PayErrorOptions } from './csp-DZPuExzu.js';
|
|
3
|
+
export { B as BuildPayChoicesOptions, a as PayChoices, b as PayErrorBody, d as PaymentCspMode, e as PaymentCspOptions, V as VoucherRowLabels, i as buildPayChoices, j as cashProviderData, k as checkoutToAttempt, l as choiceValue, p as parseChoice, m as payErrorResponse, n as paymentCsp, o as paymentCspHeaderName, q as paymentCspHeaders, r as paymentCspMode, s as paymentNonce, t as paymentNonceFromHeaders, v as safeHttpStatus, w as voucherRows } from './csp-DZPuExzu.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Server side of the embedded payment (`@asteby/storefront-react/payments/server`): the two JSON resource routes the
|
|
@@ -37,6 +37,24 @@ declare function sameOrigin(request: Request, site: string): boolean;
|
|
|
37
37
|
/** Device fingerprint (anti-fraud): OPTIONAL, so it never breaks a payment. Anything over 200 chars or odd is dropped. */
|
|
38
38
|
declare const DEVICE_SESSION_ID_MAX = 200;
|
|
39
39
|
declare function deviceSessionId(v: unknown): string | undefined;
|
|
40
|
+
type JsonPayRouteOptions = PayErrorOptions & {
|
|
41
|
+
/** Cache header of the response. Default `private, no-store`. */
|
|
42
|
+
cacheControl?: string;
|
|
43
|
+
/** Diagnostic line for an exception inside the route. Default: one JSON line with `console.warn`. */
|
|
44
|
+
log?: (entry: {
|
|
45
|
+
level: 'warn';
|
|
46
|
+
msg: string;
|
|
47
|
+
where: string;
|
|
48
|
+
error: string;
|
|
49
|
+
}) => void;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Runs a payment route and guarantees a JSON answer: `{ error: { code, message, retryable, kind } }` with a status the
|
|
53
|
+
* proxy leaves alone (never 502-504, which Traefik/CDNs replace with an HTML page, dropping the JSON and the
|
|
54
|
+
* `Set-Cookie` of the payment session). An exception becomes a transient, retryable error (same idempotency key).
|
|
55
|
+
* `createEmbeddedPayHandlers` already applies it; use it for routes of your own.
|
|
56
|
+
*/
|
|
57
|
+
declare function jsonPayRoute(run: () => Promise<Response>, where: string, options?: JsonPayRouteOptions | string): Promise<Response>;
|
|
40
58
|
/** What the shop keeps between the checkout creation and the end of the payment (usually in a cookie). */
|
|
41
59
|
type PaySession<TDraft extends {
|
|
42
60
|
checkout_id?: string;
|
|
@@ -95,6 +113,8 @@ type EmbeddedPayConfig<TDraft extends {
|
|
|
95
113
|
/** Method keys the shop never offers (the server also checks them: a forged `payment_choice` is refused). */
|
|
96
114
|
hide?: Iterable<string>;
|
|
97
115
|
messages?: Partial<EmbeddedPayMessages>;
|
|
116
|
+
/** Replaces the default texts of the error bodies (`error.<kind>`), e.g. to speak in the shop's voice. */
|
|
117
|
+
errorMessages?: Partial<PaymentRecoveryMessages>;
|
|
98
118
|
/** Per-IP limit on charges. Default 10 per 10 minutes; `false` disables (OPS still caps attempts per checkout). */
|
|
99
119
|
rateLimit?: RateLimiter | {
|
|
100
120
|
limit?: number;
|
|
@@ -142,4 +162,4 @@ declare function createEmbeddedPayHandlers<TDraft extends {
|
|
|
142
162
|
checkout_id?: string;
|
|
143
163
|
}>(config: EmbeddedPayConfig<TDraft>): EmbeddedPayHandlers;
|
|
144
164
|
|
|
145
|
-
export { type CashOutcome, DEVICE_SESSION_ID_MAX, type EmbeddedPayConfig, type EmbeddedPayHandlers, type EmbeddedPayMessages, PayChoice, type PaySession, type RateLimiter, clientKey, createEmbeddedPayHandlers, createRateLimiter, deviceSessionId, sameOrigin };
|
|
165
|
+
export { type CashOutcome, DEVICE_SESSION_ID_MAX, type EmbeddedPayConfig, type EmbeddedPayHandlers, type EmbeddedPayMessages, type JsonPayRouteOptions, PayChoice, PayErrorOptions, type PaySession, type RateLimiter, clientKey, createEmbeddedPayHandlers, createRateLimiter, deviceSessionId, jsonPayRoute, sameOrigin };
|
package/dist/payments-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { parseChoice, buildPayChoices,
|
|
2
|
-
export { buildPayChoices, cashProviderData, checkoutToAttempt, choiceValue, parseChoice, payErrorResponse, paymentCsp, paymentCspHeaderName, paymentCspHeaders, paymentCspMode, paymentNonce, paymentNonceFromHeaders, voucherRows } from './chunk-
|
|
3
|
-
import { paymentIdempotencyKey, isStorefrontError, paymentOutcome, safeHttpsUrl } from '@asteby/storefront';
|
|
1
|
+
import { payErrorResponse, safeHttpStatus, parseChoice, buildPayChoices, checkoutToAttempt, cashProviderData } from './chunk-MNLROALF.js';
|
|
2
|
+
export { buildPayChoices, cashProviderData, checkoutToAttempt, choiceValue, parseChoice, payErrorResponse, paymentCsp, paymentCspHeaderName, paymentCspHeaders, paymentCspMode, paymentNonce, paymentNonceFromHeaders, safeHttpStatus, voucherRows } from './chunk-MNLROALF.js';
|
|
3
|
+
import { PAYMENT_CHOICE_REFUSED_FIELD, paymentIdempotencyKey, isStorefrontError, paymentOutcome, safeHttpsUrl } from '@asteby/storefront';
|
|
4
4
|
|
|
5
5
|
function createRateLimiter({ limit, windowMs }) {
|
|
6
6
|
const hits = /* @__PURE__ */ new Map();
|
|
@@ -42,6 +42,53 @@ function deviceSessionId(v) {
|
|
|
42
42
|
const id = v.trim();
|
|
43
43
|
return id.length > 0 && id.length <= DEVICE_SESSION_ID_MAX && /^[\w.:-]+$/.test(id) ? id : void 0;
|
|
44
44
|
}
|
|
45
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
46
|
+
function copyHeaders(from) {
|
|
47
|
+
const out = new Headers();
|
|
48
|
+
from.forEach((value, key) => {
|
|
49
|
+
const k = key.toLowerCase();
|
|
50
|
+
if (k === "content-length" || k === "content-type" || k === "set-cookie" || k === "refresh") return;
|
|
51
|
+
out.set(key, value);
|
|
52
|
+
});
|
|
53
|
+
for (const cookie of from.getSetCookie?.() ?? []) out.append("Set-Cookie", cookie);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
async function jsonPayRoute(run, where, options = {}) {
|
|
57
|
+
const opts = typeof options === "string" ? { cacheControl: options } : options;
|
|
58
|
+
const cache = opts.cacheControl ?? "private, no-store";
|
|
59
|
+
const errorOptions = { ...opts.locale ? { locale: opts.locale } : {}, ...opts.messages ? { messages: opts.messages } : {} };
|
|
60
|
+
let res;
|
|
61
|
+
try {
|
|
62
|
+
res = await run();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const entry = { level: "warn", msg: "excepci\xF3n en la ruta de pago", where, error: String(err?.message ?? err).slice(0, 300) };
|
|
65
|
+
if (opts.log) opts.log(entry);
|
|
66
|
+
else console.warn(JSON.stringify(entry));
|
|
67
|
+
const { status: status2, body: body2 } = payErrorResponse({ status: 500, code: "server_error" }, errorOptions);
|
|
68
|
+
return new Response(JSON.stringify(body2), { status: status2, headers: { ...JSON_HEADERS, "Cache-Control": cache } });
|
|
69
|
+
}
|
|
70
|
+
const type = res.headers.get("content-type") ?? "";
|
|
71
|
+
if (res.ok && type.includes("json")) return res;
|
|
72
|
+
let parsed = null;
|
|
73
|
+
if (type.includes("json")) parsed = await res.clone().json().catch(() => null);
|
|
74
|
+
const e = parsed?.error;
|
|
75
|
+
if (!res.ok && e && typeof e.kind === "string" && safeHttpStatus(res.status) === res.status) return res;
|
|
76
|
+
const { status, body } = payErrorResponse(
|
|
77
|
+
{
|
|
78
|
+
status: res.ok ? 500 : res.status,
|
|
79
|
+
...e?.code ? { code: e.code } : {},
|
|
80
|
+
...e?.message ? { message: e.message } : {},
|
|
81
|
+
...e?.field ? { field: e.field } : {},
|
|
82
|
+
...e?.details !== void 0 ? { details: e.details } : {},
|
|
83
|
+
...typeof e?.retryable === "boolean" ? { retryable: e.retryable } : {}
|
|
84
|
+
},
|
|
85
|
+
errorOptions
|
|
86
|
+
);
|
|
87
|
+
const headers = copyHeaders(res.headers);
|
|
88
|
+
headers.set("Content-Type", "application/json");
|
|
89
|
+
headers.set("Cache-Control", cache);
|
|
90
|
+
return new Response(JSON.stringify(body), { status, headers });
|
|
91
|
+
}
|
|
45
92
|
var ATTEMPT = /^[A-Za-z0-9_-]{8,100}$/;
|
|
46
93
|
var asMoney = (v) => {
|
|
47
94
|
const m = v;
|
|
@@ -74,10 +121,14 @@ function createEmbeddedPayHandlers(config) {
|
|
|
74
121
|
headers.set("Content-Type", "application/json");
|
|
75
122
|
return new Response(JSON.stringify(body), { status: init.status ?? 200, headers });
|
|
76
123
|
};
|
|
77
|
-
const
|
|
124
|
+
const errorOptions = { locale, ...config.errorMessages ? { messages: config.errorMessages } : {} };
|
|
125
|
+
const fail = (status2, code, message, extra = {}, more = {}) => {
|
|
126
|
+
const r = payErrorResponse({ status: status2, code, message, ...more.field ? { field: more.field } : {} }, { ...errorOptions, ownMessage: true, ...more.kind ? { kind: more.kind } : {} });
|
|
127
|
+
return json(r.body, { status: r.status, headers: extra });
|
|
128
|
+
};
|
|
78
129
|
async function pay(request) {
|
|
79
130
|
const site = config.site(request);
|
|
80
|
-
if (!sameOrigin(request, site)) return fail(403, "forbidden", "origin");
|
|
131
|
+
if (!sameOrigin(request, site)) return fail(403, "forbidden", "origin", {}, { kind: "other" });
|
|
81
132
|
if (limiter) {
|
|
82
133
|
const gate = limiter.hit(clientKey(request, config.trustProxy));
|
|
83
134
|
if (!gate.allowed) return fail(429, "too_many_attempts", msg.tooManyRequests, { "Retry-After": String(gate.retryAfterSec) });
|
|
@@ -107,7 +158,7 @@ function createEmbeddedPayHandlers(config) {
|
|
|
107
158
|
try {
|
|
108
159
|
const methods = await sf.carts.paymentMethods(cartId);
|
|
109
160
|
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);
|
|
161
|
+
if (!choice || !card) return fail(422, "invalid_state", msg.methodUnavailable, {}, { field: PAYMENT_CHOICE_REFUSED_FIELD });
|
|
111
162
|
const previous = await config.readSession(request);
|
|
112
163
|
const reusedId = previous && previous.cart_id === cartId ? previous.draft.checkout_id : void 0;
|
|
113
164
|
let checkoutId;
|
|
@@ -137,7 +188,7 @@ function createEmbeddedPayHandlers(config) {
|
|
|
137
188
|
return json(result, { headers });
|
|
138
189
|
} catch (err) {
|
|
139
190
|
if (isStorefrontError(err)) {
|
|
140
|
-
const { status: status2, body: errBody } = payErrorResponse(err);
|
|
191
|
+
const { status: status2, body: errBody } = payErrorResponse(err, errorOptions);
|
|
141
192
|
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
193
|
const headers = new Headers();
|
|
143
194
|
if (session) headers.append("Set-Cookie", await config.serializeSession(session));
|
|
@@ -147,21 +198,22 @@ function createEmbeddedPayHandlers(config) {
|
|
|
147
198
|
}
|
|
148
199
|
}
|
|
149
200
|
async function status(request) {
|
|
150
|
-
if (!sameOrigin(request, config.site(request))) return fail(403, "forbidden", "origin");
|
|
201
|
+
if (!sameOrigin(request, config.site(request))) return fail(403, "forbidden", "origin", {}, { kind: "other" });
|
|
151
202
|
const session = await config.readSession(request);
|
|
152
203
|
const id = session?.draft.checkout_id;
|
|
153
|
-
|
|
204
|
+
const none = async () => json({ payment: { provider: "", status: "none" }, recoverable: Boolean(await config.readCartId(request)) });
|
|
205
|
+
if (!session || !id) return none();
|
|
154
206
|
try {
|
|
155
207
|
const sf = await config.client(request);
|
|
156
208
|
const found = await sf.checkouts.get(id);
|
|
157
|
-
if (!found) return
|
|
209
|
+
if (!found) return none();
|
|
158
210
|
const attempt = checkoutToAttempt(found);
|
|
159
211
|
const headers = new Headers();
|
|
160
212
|
await config.settle({ draft: session.draft, result: attempt, headers });
|
|
161
213
|
return json(attempt, { headers });
|
|
162
214
|
} catch (err) {
|
|
163
215
|
if (isStorefrontError(err)) {
|
|
164
|
-
const { status: code, body } = payErrorResponse(err);
|
|
216
|
+
const { status: code, body } = payErrorResponse(err, errorOptions);
|
|
165
217
|
return json(body, { status: code });
|
|
166
218
|
}
|
|
167
219
|
throw err;
|
|
@@ -183,9 +235,14 @@ function createEmbeddedPayHandlers(config) {
|
|
|
183
235
|
const outcome = paymentOutcome(result, locale);
|
|
184
236
|
return { kind: "error", status: safeHttpsUrl(outcome.redirect_url) ? 409 : 422, message: outcome.message, fields: [] };
|
|
185
237
|
}
|
|
186
|
-
|
|
238
|
+
const routeOptions = { cacheControl: cache, ...errorOptions };
|
|
239
|
+
return {
|
|
240
|
+
pay: (request) => jsonPayRoute(() => pay(request), "pay", routeOptions),
|
|
241
|
+
status: (request) => jsonPayRoute(() => status(request), "status", routeOptions),
|
|
242
|
+
cash
|
|
243
|
+
};
|
|
187
244
|
}
|
|
188
245
|
|
|
189
|
-
export { DEVICE_SESSION_ID_MAX, clientKey, createEmbeddedPayHandlers, createRateLimiter, deviceSessionId, sameOrigin };
|
|
246
|
+
export { DEVICE_SESSION_ID_MAX, clientKey, createEmbeddedPayHandlers, createRateLimiter, deviceSessionId, jsonPayRoute, sameOrigin };
|
|
190
247
|
//# sourceMappingURL=payments-server.js.map
|
|
191
248
|
//# sourceMappingURL=payments-server.js.map
|
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../src/payments/server.ts"],"names":["status","body"],"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 PAYMENT_CHOICE_REFUSED_FIELD,\n isStorefrontError,\n paymentIdempotencyKey,\n paymentOutcome,\n safeHttpsUrl,\n type Cart,\n type CheckoutResult,\n type Money,\n type PaymentAttemptResult,\n type PaymentErrorClassKind,\n type StorefrontClient,\n} from '@asteby/storefront'\nimport { buildPayChoices, cashProviderData, checkoutToAttempt, parseChoice, payErrorResponse, safeHttpStatus, type PayChoice, type PayErrorOptions } from './choices.js'\nimport type { PaymentRecoveryMessages } from './recovery-messages.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 JSON_HEADERS = { 'Content-Type': 'application/json' }\n\nexport type JsonPayRouteOptions = PayErrorOptions & {\n /** Cache header of the response. Default `private, no-store`. */\n cacheControl?: string\n /** Diagnostic line for an exception inside the route. Default: one JSON line with `console.warn`. */\n log?: (entry: { level: 'warn'; msg: string; where: string; error: string }) => void\n}\n\nfunction copyHeaders(from: Headers): Headers {\n const out = new Headers()\n from.forEach((value, key) => {\n const k = key.toLowerCase()\n if (k === 'content-length' || k === 'content-type' || k === 'set-cookie' || k === 'refresh') return\n out.set(key, value)\n })\n for (const cookie of from.getSetCookie?.() ?? []) out.append('Set-Cookie', cookie)\n return out\n}\n\n/**\n * Runs a payment route and guarantees a JSON answer: `{ error: { code, message, retryable, kind } }` with a status the\n * proxy leaves alone (never 502-504, which Traefik/CDNs replace with an HTML page, dropping the JSON and the\n * `Set-Cookie` of the payment session). An exception becomes a transient, retryable error (same idempotency key).\n * `createEmbeddedPayHandlers` already applies it; use it for routes of your own.\n */\nexport async function jsonPayRoute(run: () => Promise<Response>, where: string, options: JsonPayRouteOptions | string = {}): Promise<Response> {\n const opts: JsonPayRouteOptions = typeof options === 'string' ? { cacheControl: options } : options\n const cache = opts.cacheControl ?? 'private, no-store'\n const errorOptions: PayErrorOptions = { ...(opts.locale ? { locale: opts.locale } : {}), ...(opts.messages ? { messages: opts.messages } : {}) }\n let res: Response\n try {\n res = await run()\n } catch (err) {\n const entry = { level: 'warn' as const, msg: 'excepción en la ruta de pago', where, error: String((err as Error)?.message ?? err).slice(0, 300) }\n if (opts.log) opts.log(entry)\n else console.warn(JSON.stringify(entry))\n const { status, body } = payErrorResponse({ status: 500, code: 'server_error' }, errorOptions)\n return new Response(JSON.stringify(body), { status, headers: { ...JSON_HEADERS, 'Cache-Control': cache } })\n }\n const type = res.headers.get('content-type') ?? ''\n if (res.ok && type.includes('json')) return res\n\n let parsed: { error?: { code?: string; message?: string; retryable?: boolean; kind?: string; field?: string; details?: unknown } } | null = null\n if (type.includes('json')) parsed = await res.clone().json().catch(() => null)\n const e = parsed?.error\n // Already normalised by the shop (has `kind`) and with a status the proxy keeps: leave it as it is.\n if (!res.ok && e && typeof e.kind === 'string' && safeHttpStatus(res.status) === res.status) return res\n const { status, body } = payErrorResponse(\n {\n status: res.ok ? 500 : res.status,\n ...(e?.code ? { code: e.code } : {}),\n ...(e?.message ? { message: e.message } : {}),\n ...(e?.field ? { field: e.field } : {}),\n ...(e?.details !== undefined ? { details: e.details } : {}),\n ...(typeof e?.retryable === 'boolean' ? { retryable: e.retryable } : {}),\n },\n errorOptions,\n )\n const headers = copyHeaders(res.headers)\n headers.set('Content-Type', 'application/json')\n headers.set('Cache-Control', cache)\n return new Response(JSON.stringify(body), { status, headers })\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 /** Replaces the default texts of the error bodies (`error.<kind>`), e.g. to speak in the shop's voice. */\n errorMessages?: Partial<PaymentRecoveryMessages>\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 errorOptions: PayErrorOptions = { locale, ...(config.errorMessages ? { messages: config.errorMessages } : {}) }\n /** The shop's own refusal, in the same body shape (`retryable`, `kind`) as an OPS failure. */\n const fail = (status: number, code: string, message: string, extra: Headers | Record<string, string> = {}, more: { field?: string; kind?: PaymentErrorClassKind } = {}) => {\n const r = payErrorResponse({ status, code, message, ...(more.field ? { field: more.field } : {}) }, { ...errorOptions, ownMessage: true, ...(more.kind ? { kind: more.kind } : {}) })\n return json(r.body, { status: r.status, headers: extra })\n }\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', {}, { kind: 'other' })\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, {}, { field: PAYMENT_CHOICE_REFUSED_FIELD })\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, errorOptions)\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', {}, { kind: 'other' })\n const session = await config.readSession(request)\n const id = session?.draft.checkout_id\n // No payment session in this browser (cookie lost or never set). It is NOT an error and NOT a result: `none` tells the\n // transport there is nothing to read, so an unknown outcome stays unknown and is repeated with the SAME key.\n // `recoverable`: the shop still has the buyer's cart, so that same-key retry can proceed.\n const none = async (): Promise<Response> => json({ payment: { provider: '', status: 'none' }, recoverable: Boolean(await config.readCartId(request)) })\n if (!session || !id) return none()\n try {\n const sf = await config.client(request)\n const found = await sf.checkouts.get(id)\n if (!found) return none()\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, errorOptions)\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 const routeOptions: JsonPayRouteOptions = { cacheControl: cache, ...errorOptions }\n return {\n pay: (request) => jsonPayRoute(() => pay(request), 'pay', routeOptions),\n status: (request) => jsonPayRoute(() => status(request), 'status', routeOptions),\n cash,\n }\n}\n"]}
|