@proxy-checkout/stripe-server-js 0.1.0-prx-146.161.1 → 0.1.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 +15 -113
- package/dist/cjs/errors.d.cts +1 -68
- package/dist/cjs/errors.d.ts +1 -68
- package/dist/cjs/errors.js +1 -86
- package/dist/cjs/index.d.cts +4 -10
- package/dist/cjs/index.d.ts +4 -10
- package/dist/cjs/index.js +1 -24
- package/dist/cjs/metadata.d.cts +2 -26
- package/dist/cjs/metadata.d.ts +2 -26
- package/dist/cjs/metadata.js +3 -73
- package/dist/cjs/open-checkout.d.cts +0 -21
- package/dist/cjs/open-checkout.d.ts +0 -21
- package/dist/cjs/open-checkout.js +38 -310
- package/dist/cjs/sync-checkout-cart.d.cts +3 -3
- package/dist/cjs/sync-checkout-cart.d.ts +3 -3
- package/dist/cjs/sync-checkout-cart.js +22 -92
- package/dist/cjs/types.d.cts +0 -199
- package/dist/cjs/types.d.ts +0 -199
- package/dist/cjs/version.d.cts +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/errors.d.ts +1 -68
- package/dist/esm/errors.js +0 -80
- package/dist/esm/index.d.ts +4 -10
- package/dist/esm/index.js +2 -7
- package/dist/esm/metadata.d.ts +2 -26
- package/dist/esm/metadata.js +2 -69
- package/dist/esm/open-checkout.d.ts +0 -21
- package/dist/esm/open-checkout.js +40 -312
- package/dist/esm/sync-checkout-cart.d.ts +3 -3
- package/dist/esm/sync-checkout-cart.js +23 -93
- package/dist/esm/types.d.ts +0 -199
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +3 -3
- package/dist/cjs/acquisition.d.cts +0 -189
- package/dist/cjs/acquisition.d.ts +0 -189
- package/dist/cjs/acquisition.js +0 -789
- package/dist/cjs/compatibility.d.cts +0 -66
- package/dist/cjs/compatibility.d.ts +0 -66
- package/dist/cjs/compatibility.js +0 -174
- package/dist/cjs/open-payment-intent.d.cts +0 -35
- package/dist/cjs/open-payment-intent.d.ts +0 -35
- package/dist/cjs/open-payment-intent.js +0 -157
- package/dist/cjs/open-setup-intent.d.cts +0 -49
- package/dist/cjs/open-setup-intent.d.ts +0 -49
- package/dist/cjs/open-setup-intent.js +0 -157
- package/dist/cjs/open-subscription.d.cts +0 -75
- package/dist/cjs/open-subscription.d.ts +0 -75
- package/dist/cjs/open-subscription.js +0 -307
- package/dist/cjs/subscription-params.d.cts +0 -33
- package/dist/cjs/subscription-params.d.ts +0 -33
- package/dist/cjs/subscription-params.js +0 -237
- package/dist/esm/acquisition.d.ts +0 -189
- package/dist/esm/acquisition.js +0 -763
- package/dist/esm/compatibility.d.ts +0 -66
- package/dist/esm/compatibility.js +0 -168
- package/dist/esm/open-payment-intent.d.ts +0 -35
- package/dist/esm/open-payment-intent.js +0 -154
- package/dist/esm/open-setup-intent.d.ts +0 -49
- package/dist/esm/open-setup-intent.js +0 -153
- package/dist/esm/open-subscription.d.ts +0 -75
- package/dist/esm/open-subscription.js +0 -304
- package/dist/esm/subscription-params.d.ts +0 -33
- package/dist/esm/subscription-params.js +0 -232
|
@@ -7,15 +7,10 @@
|
|
|
7
7
|
* metadata placement, idempotency, and binding reconciliation.
|
|
8
8
|
*/
|
|
9
9
|
import { assertCartBuyerReference, ProxyCheckoutValidationError } from "@proxy-checkout/server-js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { StripeResponseShapeError, UnsupportedStripeOptionError } from "./errors.js";
|
|
13
|
-
import { assertCheckoutSessionBelongsToProxy, assertCheckoutSessionMetadataPreflight, assertProviderObjectBelongsToAcquisition, injectRequiredCheckoutSessionMetadata, } from "./metadata.js";
|
|
10
|
+
import { ProxyStripeOpenCheckoutError } from "./errors.js";
|
|
11
|
+
import { assertCheckoutSessionBelongsToProxy, injectRequiredCheckoutSessionMetadata, } from "./metadata.js";
|
|
14
12
|
import { normalizePsp } from "./psp.js";
|
|
15
13
|
export async function openCheckout(options) {
|
|
16
|
-
if (usesAcquisitionContract(options)) {
|
|
17
|
-
return openAcquisitionCheckout(options);
|
|
18
|
-
}
|
|
19
14
|
// Normalize the same way the API does on store, so the binding is recorded
|
|
20
15
|
// under the normalized psp and later syncCheckoutCart validations compare
|
|
21
16
|
// normalized-to-normalized.
|
|
@@ -135,7 +130,7 @@ export async function openCheckout(options) {
|
|
|
135
130
|
const checkoutSession = (await options.stripe.checkout.sessions.create(injectRequiredCheckoutSessionMetadata(params, proxySession.id), buildStripeRequestOptions(options, proxySession.id, psp)));
|
|
136
131
|
// Fail closed if the merchant forgot to place proxy_session_id metadata.
|
|
137
132
|
assertCheckoutSessionBelongsToProxy(checkoutSession, proxySession.id);
|
|
138
|
-
const binding = await
|
|
133
|
+
const binding = await recordOrReconcileProviderBinding({
|
|
139
134
|
checkoutSessionId: checkoutSession.id,
|
|
140
135
|
expectedCartVersion: proxySession.cartVersion,
|
|
141
136
|
commercialMode,
|
|
@@ -157,310 +152,6 @@ export async function openCheckout(options) {
|
|
|
157
152
|
proxySessionId: proxySession.id,
|
|
158
153
|
};
|
|
159
154
|
}
|
|
160
|
-
async function openAcquisitionCheckout(options) {
|
|
161
|
-
if (options.commercialMode !== "one_time" ||
|
|
162
|
-
(options.uiMode !== "hosted" && options.uiMode !== "embedded" && options.uiMode !== "custom") ||
|
|
163
|
-
options.pricingMode === undefined) {
|
|
164
|
-
throw new UnsupportedStripeOptionError("The explicit Checkout acquisition contract supports one_time hosted, embedded, and custom presentations only.", "uiMode");
|
|
165
|
-
}
|
|
166
|
-
if (normalizePsp(options.psp ?? "stripe") !== "stripe") {
|
|
167
|
-
throw new UnsupportedStripeOptionError("The Stripe acquisition helper cannot route provider creation through another PSP.", "psp");
|
|
168
|
-
}
|
|
169
|
-
assertNoConnectRequestOptions(options.stripeRequestOptions);
|
|
170
|
-
const compatibility = compatibilityDescriptor(options);
|
|
171
|
-
assertStripePathCompatibility(checkoutCompatibilityPath(options.uiMode), compatibility);
|
|
172
|
-
let proxySession = await options.proxy.sessions.payerOpened(options.proxySessionId, {
|
|
173
|
-
requestId: options.requestId,
|
|
174
|
-
});
|
|
175
|
-
let cart = parseCheckoutCart(options, proxySession);
|
|
176
|
-
if (proxySession.presentationState !== "checkout_available") {
|
|
177
|
-
return terminalResultFromSession(proxySession, cart);
|
|
178
|
-
}
|
|
179
|
-
let offer = options.validateCart
|
|
180
|
-
? await options.validateCart({ cart, session: proxySession })
|
|
181
|
-
: undefined;
|
|
182
|
-
const reconciledCart = await options.reconcileCart?.({ cart, offer, session: proxySession });
|
|
183
|
-
if (reconciledCart) {
|
|
184
|
-
if (options.getCartBuyerReference) {
|
|
185
|
-
assertCartBuyerReference(options.getCartBuyerReference(reconciledCart.cart), proxySession);
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
const updated = await options.proxy.sessions.cart.set(proxySession.id, {
|
|
189
|
-
amountMinor: reconciledCart.amountMinor,
|
|
190
|
-
cartSnapshot: reconciledCart.cartSnapshot,
|
|
191
|
-
currency: reconciledCart.currency,
|
|
192
|
-
expectedCartVersion: proxySession.cartVersion,
|
|
193
|
-
requestId: options.requestId,
|
|
194
|
-
});
|
|
195
|
-
proxySession = {
|
|
196
|
-
...proxySession,
|
|
197
|
-
amountMinor: updated.amountMinor,
|
|
198
|
-
cartSnapshot: updated.cartSnapshot,
|
|
199
|
-
cartVersion: updated.cartVersion,
|
|
200
|
-
currency: updated.currency,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
catch (cause) {
|
|
204
|
-
if (!isProxyConflict(cause))
|
|
205
|
-
throw cause;
|
|
206
|
-
const joined = await options.proxy.sessions.payerOpened(proxySession.id, {
|
|
207
|
-
requestId: options.requestId,
|
|
208
|
-
});
|
|
209
|
-
if (!sameCheckoutCart(joined, reconciledCart))
|
|
210
|
-
throw cause;
|
|
211
|
-
proxySession = joined;
|
|
212
|
-
}
|
|
213
|
-
cart = reconciledCart.cart;
|
|
214
|
-
if (options.validateCart)
|
|
215
|
-
offer = await options.validateCart({ cart, session: proxySession });
|
|
216
|
-
}
|
|
217
|
-
const current = await options.proxy.sessions.payerOpened(proxySession.id, {
|
|
218
|
-
requestId: options.requestId,
|
|
219
|
-
});
|
|
220
|
-
if (current.presentationState !== "checkout_available") {
|
|
221
|
-
return terminalResultFromSession(current, cart);
|
|
222
|
-
}
|
|
223
|
-
if (!sameCheckoutCart(current, proxySession)) {
|
|
224
|
-
throw new ProxyCheckoutValidationError(`Proxy session ${proxySession.id} cart changed while Checkout was being prepared.`, { code: "cart_version_conflict", field: "cart_version" });
|
|
225
|
-
}
|
|
226
|
-
proxySession = current;
|
|
227
|
-
const builtParams = await options.buildCheckoutSessionParams({
|
|
228
|
-
cart,
|
|
229
|
-
offer,
|
|
230
|
-
proxySession,
|
|
231
|
-
stripe: options.stripe,
|
|
232
|
-
});
|
|
233
|
-
validateAcquisitionCheckoutParams(builtParams, options, proxySession);
|
|
234
|
-
assertCheckoutSessionMetadataPreflight(builtParams, proxySession.id);
|
|
235
|
-
// Stripe 12 has no hosted ui_mode create field, while newer Stripe responses
|
|
236
|
-
// name the same provider value hosted_page. Omit only the hosted provider
|
|
237
|
-
// alias; client-secret paths must send their exact generation-specific mode.
|
|
238
|
-
const params = options.uiMode === "hosted" ? withoutHostedProviderUiMode(builtParams) : builtParams;
|
|
239
|
-
if (options.pricingMode === "fixed" && proxySession.amountMinor === 0) {
|
|
240
|
-
assertHostedNoCostCompatibility(compatibility);
|
|
241
|
-
}
|
|
242
|
-
const providerOptionsFingerprint = fingerprintHostedCheckoutProviderOptions(params);
|
|
243
|
-
const acquisitionResult = await acquireStripeRoot({
|
|
244
|
-
checkoutUiMode: options.uiMode,
|
|
245
|
-
commercialMode: "one_time",
|
|
246
|
-
integrationPath: "checkout_session",
|
|
247
|
-
objectType: "checkout_session",
|
|
248
|
-
pricingMode: options.pricingMode,
|
|
249
|
-
create: async (reservation) => {
|
|
250
|
-
const created = (await options.stripe.checkout.sessions.create(injectRequiredCheckoutSessionMetadata(params, proxySession.id, {
|
|
251
|
-
contractVersion: "2",
|
|
252
|
-
id: reservation.acquisitionAttempt.id,
|
|
253
|
-
}), {
|
|
254
|
-
...options.stripeRequestOptions,
|
|
255
|
-
idempotencyKey: reservation.stripe.idempotencyKeys.root,
|
|
256
|
-
}));
|
|
257
|
-
return (await retrieveCheckoutSessionWithLineEvidence({
|
|
258
|
-
id: created.id,
|
|
259
|
-
initial: created,
|
|
260
|
-
requestOptions: options.stripeRequestOptions,
|
|
261
|
-
stripe: options.stripe,
|
|
262
|
-
}));
|
|
263
|
-
},
|
|
264
|
-
providerOptionsFingerprint,
|
|
265
|
-
proxy: options.proxy,
|
|
266
|
-
proxySession,
|
|
267
|
-
requestId: options.requestId,
|
|
268
|
-
retrieve: async (id) => (await retrieveCheckoutSessionWithLineEvidence({
|
|
269
|
-
id,
|
|
270
|
-
requestOptions: options.stripeRequestOptions,
|
|
271
|
-
stripe: options.stripe,
|
|
272
|
-
})),
|
|
273
|
-
toConfirmation: (session) => checkoutCartConfirmation({
|
|
274
|
-
cartVersion: proxySession.cartVersion,
|
|
275
|
-
checkoutSession: session,
|
|
276
|
-
expectedLineItems: params.line_items,
|
|
277
|
-
}),
|
|
278
|
-
validateObject: (session, acquisition) => {
|
|
279
|
-
assertProviderObjectBelongsToAcquisition(session, {
|
|
280
|
-
acquisitionAttemptId: acquisition.id,
|
|
281
|
-
integrationPath: "checkout_session",
|
|
282
|
-
proxySessionId: proxySession.id,
|
|
283
|
-
});
|
|
284
|
-
assertCheckoutProviderCart(session, proxySession, options.pricingMode ?? "fixed", params.line_items, options.uiMode ?? "hosted");
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
if (acquisitionResult.outcome === "terminal") {
|
|
288
|
-
return terminalResultFromSession(acquisitionResult.proxySession, cart);
|
|
289
|
-
}
|
|
290
|
-
const acquired = acquisitionResult;
|
|
291
|
-
const presentation = checkoutPresentation(acquired.object, options.uiMode);
|
|
292
|
-
return {
|
|
293
|
-
acquisitionAttemptId: acquired.acquisition.id,
|
|
294
|
-
binding: {
|
|
295
|
-
providerCheckoutSessionId: acquired.object.id,
|
|
296
|
-
proxySessionId: proxySession.id,
|
|
297
|
-
psp: "stripe",
|
|
298
|
-
updatedAt: acquired.acquisition.updatedAt,
|
|
299
|
-
},
|
|
300
|
-
cart,
|
|
301
|
-
checkoutSession: acquired.object,
|
|
302
|
-
checkoutSessionId: acquired.object.id,
|
|
303
|
-
clientSecret: presentation.kind === "client_secret" ? presentation.clientSecret : null,
|
|
304
|
-
offer,
|
|
305
|
-
outcome: "ready",
|
|
306
|
-
presentation,
|
|
307
|
-
proxySession,
|
|
308
|
-
proxySessionId: proxySession.id,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
function usesAcquisitionContract(options) {
|
|
312
|
-
const values = [options.commercialMode, options.pricingMode, options.uiMode];
|
|
313
|
-
const count = values.filter((value) => value !== undefined).length;
|
|
314
|
-
if (count !== 0 && count !== values.length) {
|
|
315
|
-
throw new ProxyCheckoutValidationError("commercialMode, pricingMode, and uiMode must be supplied together for the acquisition contract.", { code: "unsupported_provider_option", field: "commercialMode" });
|
|
316
|
-
}
|
|
317
|
-
return count === values.length;
|
|
318
|
-
}
|
|
319
|
-
function parseCheckoutCart(options, proxySession) {
|
|
320
|
-
return options.cartSchema || options.getCartBuyerReference
|
|
321
|
-
? options.proxy.sessions.parseCart(proxySession, options.cartSchema ?? ((value) => value), { getBuyerReference: options.getCartBuyerReference })
|
|
322
|
-
: proxySession.cartSnapshot;
|
|
323
|
-
}
|
|
324
|
-
function compatibilityDescriptor(options) {
|
|
325
|
-
if (!options.compatibility && !options.stripeApiVersion)
|
|
326
|
-
return undefined;
|
|
327
|
-
return {
|
|
328
|
-
...options.compatibility,
|
|
329
|
-
...(options.stripeApiVersion ? { apiVersion: options.stripeApiVersion } : {}),
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
function validateAcquisitionCheckoutParams(params, options, proxySession) {
|
|
333
|
-
assertCheckoutAutomaticRecoveryDisabled(params, proxySession.id);
|
|
334
|
-
if (checkoutCommercialMode(params, proxySession.id) !== options.commercialMode) {
|
|
335
|
-
throw new UnsupportedStripeOptionError("Stripe Checkout mode does not match commercialMode.", "mode");
|
|
336
|
-
}
|
|
337
|
-
const uiMode = normalizeProviderUiMode(params.ui_mode);
|
|
338
|
-
if (uiMode !== options.uiMode) {
|
|
339
|
-
throw new UnsupportedStripeOptionError(`Stripe Checkout ui_mode ${uiMode} does not match requested ${options.uiMode}.`, "ui_mode");
|
|
340
|
-
}
|
|
341
|
-
if (uiMode === "hosted" && !params.success_url) {
|
|
342
|
-
throw new UnsupportedStripeOptionError("Hosted Stripe Checkout requires success_url.", "success_url");
|
|
343
|
-
}
|
|
344
|
-
if (uiMode === "hosted" && params.return_url !== undefined) {
|
|
345
|
-
throw new UnsupportedStripeOptionError("Hosted Stripe Checkout uses success_url and must not set return_url.", "return_url");
|
|
346
|
-
}
|
|
347
|
-
if (uiMode !== "hosted" && !params.return_url) {
|
|
348
|
-
throw new UnsupportedStripeOptionError(`${uiMode} Stripe Checkout requires return_url.`, "return_url");
|
|
349
|
-
}
|
|
350
|
-
if (uiMode !== "hosted" && params.success_url !== undefined) {
|
|
351
|
-
throw new UnsupportedStripeOptionError(`${uiMode} Stripe Checkout uses return_url and must not set success_url.`, "success_url");
|
|
352
|
-
}
|
|
353
|
-
const paymentIntentData = objectRecord(params.payment_intent_data);
|
|
354
|
-
for (const field of [
|
|
355
|
-
"application_fee_amount",
|
|
356
|
-
"application_fee_percent",
|
|
357
|
-
"capture_method",
|
|
358
|
-
"on_behalf_of",
|
|
359
|
-
"transfer_data",
|
|
360
|
-
"transfer_group",
|
|
361
|
-
]) {
|
|
362
|
-
if (paymentIntentData[field] !== undefined) {
|
|
363
|
-
throw new UnsupportedStripeOptionError(`One-time Checkout does not support payment_intent_data.${field}.`, `payment_intent_data.${field}`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (options.pricingMode === "fixed") {
|
|
367
|
-
for (const field of [
|
|
368
|
-
"allow_promotion_codes",
|
|
369
|
-
"shipping_address_collection",
|
|
370
|
-
"shipping_options",
|
|
371
|
-
]) {
|
|
372
|
-
const value = params[field];
|
|
373
|
-
if (value !== undefined && value !== false) {
|
|
374
|
-
throw new UnsupportedStripeOptionError(`Fixed-price Checkout cannot use provider-finalized option ${field}.`, field);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
for (const field of ["adaptive_pricing", "automatic_tax"]) {
|
|
378
|
-
const value = objectRecord(params[field]);
|
|
379
|
-
if (value.enabled === true) {
|
|
380
|
-
throw new UnsupportedStripeOptionError(`Fixed-price Checkout cannot enable provider-finalized option ${field}.`, `${field}.enabled`);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
const lineItems = Array.isArray(params.line_items) ? params.line_items : [];
|
|
384
|
-
if (lineItems.some((line) => objectRecord(objectRecord(line).adjustable_quantity).enabled === true)) {
|
|
385
|
-
throw new UnsupportedStripeOptionError("Fixed-price Checkout cannot enable adjustable line-item quantities.", "line_items.adjustable_quantity.enabled");
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
function assertCheckoutProviderCart(checkoutSession, proxySession, pricingMode, expectedLineItems, requestedUiMode) {
|
|
390
|
-
const confirmation = checkoutCartConfirmation({
|
|
391
|
-
cartVersion: 0,
|
|
392
|
-
checkoutSession,
|
|
393
|
-
expectedLineItems,
|
|
394
|
-
});
|
|
395
|
-
if (checkoutSession.mode !== undefined &&
|
|
396
|
-
checkoutSession.mode !== null &&
|
|
397
|
-
checkoutSession.mode !== "payment") {
|
|
398
|
-
throw new StripeResponseShapeError("Stripe Checkout response is not payment mode.", "mode");
|
|
399
|
-
}
|
|
400
|
-
if (normalizeProviderUiMode(checkoutSession.ui_mode) !== requestedUiMode) {
|
|
401
|
-
throw new StripeResponseShapeError(`Stripe Checkout response is not ${requestedUiMode} UI mode.`, "ui_mode");
|
|
402
|
-
}
|
|
403
|
-
if (confirmation.currency !== proxySession.currency ||
|
|
404
|
-
(pricingMode === "fixed" && confirmation.amountMinor !== proxySession.amountMinor)) {
|
|
405
|
-
throw new ProxyCheckoutValidationError(`Stripe Checkout Session ${checkoutSession.id} does not represent the current Proxy cart amount and currency.`, { code: "cart_version_conflict", field: "provider_amount" });
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
function normalizeProviderUiMode(value) {
|
|
409
|
-
if (value === undefined || value === null || value === "hosted" || value === "hosted_page") {
|
|
410
|
-
return "hosted";
|
|
411
|
-
}
|
|
412
|
-
if (value === "embedded" || value === "embedded_page")
|
|
413
|
-
return "embedded";
|
|
414
|
-
if (value === "custom" || value === "elements")
|
|
415
|
-
return "custom";
|
|
416
|
-
return value;
|
|
417
|
-
}
|
|
418
|
-
function checkoutCompatibilityPath(uiMode) {
|
|
419
|
-
return `one_time.checkout.${uiMode}`;
|
|
420
|
-
}
|
|
421
|
-
function checkoutPresentation(checkoutSession, uiMode) {
|
|
422
|
-
const clientSecret = checkoutSession.client_secret;
|
|
423
|
-
const url = checkoutSession.url;
|
|
424
|
-
if (uiMode === "hosted") {
|
|
425
|
-
if (!isSafeHostedCheckoutUrl(url)) {
|
|
426
|
-
throw new StripeResponseShapeError("Hosted Stripe Checkout response did not contain a safe HTTPS redirect URL.", "url");
|
|
427
|
-
}
|
|
428
|
-
if (clientSecret !== undefined && clientSecret !== null) {
|
|
429
|
-
throw new StripeResponseShapeError("Hosted Stripe Checkout response unexpectedly contained a client secret.", "client_secret");
|
|
430
|
-
}
|
|
431
|
-
return { kind: "redirect", url };
|
|
432
|
-
}
|
|
433
|
-
if (!isCheckoutSessionClientSecret(clientSecret)) {
|
|
434
|
-
throw new StripeResponseShapeError(`${uiMode} Stripe Checkout response did not contain a valid Checkout Session client secret.`, "client_secret");
|
|
435
|
-
}
|
|
436
|
-
if (url !== undefined && url !== null) {
|
|
437
|
-
throw new StripeResponseShapeError(`${uiMode} Stripe Checkout response unexpectedly contained a hosted redirect URL.`, "url");
|
|
438
|
-
}
|
|
439
|
-
return { clientSecret, kind: "client_secret", uiMode };
|
|
440
|
-
}
|
|
441
|
-
function isCheckoutSessionClientSecret(value) {
|
|
442
|
-
return typeof value === "string" && /^cs_(?:test|live)_[A-Za-z0-9_]+$/.test(value);
|
|
443
|
-
}
|
|
444
|
-
function isSafeHostedCheckoutUrl(value) {
|
|
445
|
-
if (typeof value !== "string" || value !== value.trim())
|
|
446
|
-
return false;
|
|
447
|
-
try {
|
|
448
|
-
const parsed = new URL(value);
|
|
449
|
-
return parsed.protocol === "https:" && parsed.username === "" && parsed.password === "";
|
|
450
|
-
}
|
|
451
|
-
catch {
|
|
452
|
-
return false;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
function withoutHostedProviderUiMode(params) {
|
|
456
|
-
if (params.ui_mode === undefined)
|
|
457
|
-
return params;
|
|
458
|
-
const { ui_mode: _providerUiMode, ...providerParams } = params;
|
|
459
|
-
return providerParams;
|
|
460
|
-
}
|
|
461
|
-
function objectRecord(value) {
|
|
462
|
-
return typeof value === "object" && value !== null ? value : {};
|
|
463
|
-
}
|
|
464
155
|
function terminalResultFromSession(proxySession, cart) {
|
|
465
156
|
const common = {
|
|
466
157
|
cart,
|
|
@@ -520,6 +211,31 @@ function bindingFromSession(proxySession) {
|
|
|
520
211
|
updatedAt: proxySession.updatedAt,
|
|
521
212
|
};
|
|
522
213
|
}
|
|
214
|
+
async function recordOrReconcileProviderBinding(input) {
|
|
215
|
+
try {
|
|
216
|
+
return await input.proxy.sessions.recordProviderBinding(input.proxySessionId, {
|
|
217
|
+
commercialMode: input.commercialMode,
|
|
218
|
+
expectedCartVersion: input.expectedCartVersion,
|
|
219
|
+
providerCheckoutSessionId: input.checkoutSessionId,
|
|
220
|
+
psp: input.psp,
|
|
221
|
+
requestId: input.requestId,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (cause) {
|
|
225
|
+
const reconciled = await readReconciledBinding(input).catch(() => undefined);
|
|
226
|
+
if (reconciled) {
|
|
227
|
+
return reconciled;
|
|
228
|
+
}
|
|
229
|
+
if (isProxyConflict(cause)) {
|
|
230
|
+
throw cause;
|
|
231
|
+
}
|
|
232
|
+
throw new ProxyStripeOpenCheckoutError("Stripe Checkout Session was created but Proxy provider binding could not be recorded.", {
|
|
233
|
+
cause,
|
|
234
|
+
code: "provider_binding_failed",
|
|
235
|
+
providerCheckoutSessionId: input.checkoutSessionId,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
523
239
|
function sameCheckoutCart(actual, expected) {
|
|
524
240
|
return (actual.amountMinor === expected.amountMinor &&
|
|
525
241
|
actual.currency === expected.currency &&
|
|
@@ -551,6 +267,18 @@ function assertCheckoutAutomaticRecoveryDisabled(params, proxySessionId) {
|
|
|
551
267
|
throw new ProxyCheckoutValidationError(`Proxy session ${proxySessionId} does not allow Stripe Checkout automatic recovery because replacement roots require explicit Proxy supersession.`, { code: "unsupported_provider_option", field: "after_expiration.recovery.enabled" });
|
|
552
268
|
}
|
|
553
269
|
}
|
|
270
|
+
async function readReconciledBinding(input) {
|
|
271
|
+
const current = await input.proxy.sessions.retrieve(input.proxySessionId, {
|
|
272
|
+
requestId: input.requestId,
|
|
273
|
+
});
|
|
274
|
+
if (current.providerCheckoutSessionId !== input.checkoutSessionId ||
|
|
275
|
+
current.providerCheckoutSessionPsp !== input.psp) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
const checkoutSession = await input.stripe.checkout.sessions.retrieve(input.checkoutSessionId, undefined, input.stripeRequestOptions);
|
|
279
|
+
assertCheckoutSessionBelongsToProxy(checkoutSession, input.proxySessionId);
|
|
280
|
+
return bindingFromSession(current);
|
|
281
|
+
}
|
|
554
282
|
function isProxyConflict(error) {
|
|
555
283
|
return (typeof error === "object" &&
|
|
556
284
|
error !== null &&
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* safest order with deterministic rollback/reconciliation on failure.
|
|
7
7
|
*/
|
|
8
8
|
import { type JsonObject, type MerchantProxySession, type ProxyCartValidator, type ProxyCheckoutServerClient } from "@proxy-checkout/server-js";
|
|
9
|
-
import type { StripeCheckoutSessionLike,
|
|
9
|
+
import type { StripeCheckoutSessionLike, StripeClientLike, StripeRequestOptionsWithoutIdempotencyLike } from "./types.js";
|
|
10
10
|
/** The next cart, expressed both as Proxy state and the typed merchant cart. */
|
|
11
11
|
export interface SyncedCart<TCart> {
|
|
12
12
|
readonly amountMinor: number;
|
|
@@ -14,7 +14,7 @@ export interface SyncedCart<TCart> {
|
|
|
14
14
|
readonly cartSnapshot: JsonObject;
|
|
15
15
|
readonly currency: string;
|
|
16
16
|
}
|
|
17
|
-
export interface SyncCheckoutCartOptions<TStripe extends
|
|
17
|
+
export interface SyncCheckoutCartOptions<TStripe extends StripeClientLike, TCart, TInput> {
|
|
18
18
|
/** Compute the next cart from current state and the merchant input. */
|
|
19
19
|
readonly buildNextCart: (input: {
|
|
20
20
|
currentCart: TCart;
|
|
@@ -44,4 +44,4 @@ export interface SyncCheckoutCartResult<TCart> {
|
|
|
44
44
|
readonly checkoutSession: StripeCheckoutSessionLike;
|
|
45
45
|
readonly currency: string;
|
|
46
46
|
}
|
|
47
|
-
export declare function syncCheckoutCart<TStripe extends
|
|
47
|
+
export declare function syncCheckoutCart<TStripe extends StripeClientLike, TCart, TInput>(options: SyncCheckoutCartOptions<TStripe, TCart, TInput>): Promise<SyncCheckoutCartResult<TCart>>;
|
|
@@ -6,9 +6,8 @@
|
|
|
6
6
|
* safest order with deterministic rollback/reconciliation on failure.
|
|
7
7
|
*/
|
|
8
8
|
import { ProxyCheckoutValidationError, } from "@proxy-checkout/server-js";
|
|
9
|
-
import { assertNoConnectRequestOptions, checkoutCartConfirmation, retrieveCheckoutSessionWithLineEvidence, } from "./acquisition.js";
|
|
10
9
|
import { ProxyStripeCartSyncError } from "./errors.js";
|
|
11
|
-
import { assertCheckoutSessionBelongsToProxy
|
|
10
|
+
import { assertCheckoutSessionBelongsToProxy } from "./metadata.js";
|
|
12
11
|
import { normalizePsp } from "./psp.js";
|
|
13
12
|
export async function syncCheckoutCart(options) {
|
|
14
13
|
// Normalize the same way the API does on store, so the binding comparison is
|
|
@@ -18,32 +17,11 @@ export async function syncCheckoutCart(options) {
|
|
|
18
17
|
const session = await options.proxy.sessions.retrieve(options.proxySessionId, {
|
|
19
18
|
requestId: options.requestId,
|
|
20
19
|
});
|
|
21
|
-
// Ownership: validate the stored binding
|
|
22
|
-
//
|
|
20
|
+
// Ownership: validate against the stored binding first, then defensively
|
|
21
|
+
// against the Stripe object's own metadata.
|
|
23
22
|
assertSessionBoundTo(session, options.checkoutSessionId, psp);
|
|
24
|
-
const acquisition = session.currentAcquisitionAttempt === null
|
|
25
|
-
? null
|
|
26
|
-
: await resolveExplicitCheckoutAcquisition(options, session);
|
|
27
|
-
if (acquisition)
|
|
28
|
-
assertNoConnectRequestOptions(options.stripeRequestOptions);
|
|
29
|
-
if (acquisition?.checkoutUiMode === "hosted") {
|
|
30
|
-
throw new ProxyStripeCartSyncError("Hosted Stripe Checkout Sessions do not support line-item mutation. Expire the bound Session, wait for Proxy to observe provider-confirmed expiry, update the versioned Proxy cart, supersede the terminal acquisition, and call openCheckout again.", {
|
|
31
|
-
cause: new Error("Stripe hosted Checkout requires provider-confirmed replacement"),
|
|
32
|
-
code: "hosted_replacement_required",
|
|
33
|
-
reconciled: true,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
23
|
const stripeSession = await options.stripe.checkout.sessions.retrieve(options.checkoutSessionId, undefined, options.stripeRequestOptions);
|
|
37
|
-
|
|
38
|
-
assertProviderObjectBelongsToAcquisition(stripeSession, {
|
|
39
|
-
acquisitionAttemptId: acquisition.id,
|
|
40
|
-
integrationPath: "checkout_session",
|
|
41
|
-
proxySessionId: options.proxySessionId,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
assertCheckoutSessionBelongsToProxy(stripeSession, options.proxySessionId);
|
|
46
|
-
}
|
|
24
|
+
assertCheckoutSessionBelongsToProxy(stripeSession, options.proxySessionId);
|
|
47
25
|
const currentCart = options.cartSchema
|
|
48
26
|
? options.proxy.sessions.parseCart(session, options.cartSchema)
|
|
49
27
|
: session.cartSnapshot;
|
|
@@ -52,6 +30,12 @@ export async function syncCheckoutCart(options) {
|
|
|
52
30
|
currentSession: session,
|
|
53
31
|
input: options.input,
|
|
54
32
|
});
|
|
33
|
+
const prior = {
|
|
34
|
+
amountMinor: session.amountMinor,
|
|
35
|
+
cartSnapshot: session.cartSnapshot,
|
|
36
|
+
cartVersion: session.cartVersion,
|
|
37
|
+
currency: session.currency,
|
|
38
|
+
};
|
|
55
39
|
const stripeUpdateParams = {
|
|
56
40
|
line_items: options.buildStripeLineItems(next.cart),
|
|
57
41
|
};
|
|
@@ -73,22 +57,19 @@ export async function syncCheckoutCart(options) {
|
|
|
73
57
|
updatedCheckout = await options.stripe.checkout.sessions.update(options.checkoutSessionId, stripeUpdateParams, stripeUpdateOptions);
|
|
74
58
|
}
|
|
75
59
|
catch (retryCause) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
catch (cause) {
|
|
91
|
-
throw new ProxyStripeCartSyncError("Stripe checkout was updated, but Proxy could not confirm the provider-authored cart revision; payment remains non-provisionable until reconciliation succeeds.", { cause, code: "reconciliation_failed", reconciled: false });
|
|
60
|
+
try {
|
|
61
|
+
await options.proxy.sessions.cart.set(options.proxySessionId, {
|
|
62
|
+
amountMinor: prior.amountMinor,
|
|
63
|
+
cartSnapshot: prior.cartSnapshot,
|
|
64
|
+
currency: prior.currency,
|
|
65
|
+
expectedCartVersion: proxyCartUpdate.cartVersion,
|
|
66
|
+
requestId: options.requestId,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (rollbackCause) {
|
|
70
|
+
throw new ProxyStripeCartSyncError("Stripe checkout update failed after an idempotent retry and the Proxy cart rollback failed; carts may be out of sync.", { cause: retryCause, code: "reconciliation_failed", reconciled: false, rollbackCause });
|
|
71
|
+
}
|
|
72
|
+
throw new ProxyStripeCartSyncError("Stripe checkout update failed after an idempotent retry; the Proxy cart rollback was attempted, but Stripe state may still be ambiguous.", { cause: retryCause, code: "stripe_update_failed", reconciled: false });
|
|
92
73
|
}
|
|
93
74
|
}
|
|
94
75
|
return {
|
|
@@ -99,57 +80,6 @@ export async function syncCheckoutCart(options) {
|
|
|
99
80
|
currency: proxyCartUpdate.currency,
|
|
100
81
|
};
|
|
101
82
|
}
|
|
102
|
-
async function confirmCheckoutCartRevision(options, input) {
|
|
103
|
-
const checkoutSession = await retrieveCheckoutSessionWithLineEvidence({
|
|
104
|
-
id: options.checkoutSessionId,
|
|
105
|
-
initial: input.checkoutSession,
|
|
106
|
-
requestOptions: options.stripeRequestOptions,
|
|
107
|
-
stripe: options.stripe,
|
|
108
|
-
});
|
|
109
|
-
const confirmation = checkoutCartConfirmation({
|
|
110
|
-
cartVersion: input.cartVersion,
|
|
111
|
-
checkoutSession,
|
|
112
|
-
expectedLineItems: input.expectedLineItems,
|
|
113
|
-
});
|
|
114
|
-
const acquisition = input.acquisition;
|
|
115
|
-
if (acquisition.commercialMode !== "one_time" ||
|
|
116
|
-
acquisition.integrationPath !== "checkout_session") {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (acquisition.pricingMode === "fixed" &&
|
|
120
|
-
confirmation.amountMinor !== input.expectedAmountMinor) {
|
|
121
|
-
throw new Error("Stripe Checkout total does not match the fixed Proxy cart amount.");
|
|
122
|
-
}
|
|
123
|
-
if (confirmation.currency !== input.currency) {
|
|
124
|
-
throw new Error("Stripe Checkout currency does not match the Proxy cart currency.");
|
|
125
|
-
}
|
|
126
|
-
await options.proxy.providerAcquisitions.confirmProviderAcquisitionCart(options.proxySessionId, acquisition.id, {
|
|
127
|
-
...confirmation,
|
|
128
|
-
expectedVersion: acquisition.version,
|
|
129
|
-
requestId: options.requestId,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
async function resolveExplicitCheckoutAcquisition(options, session) {
|
|
133
|
-
const summary = session.currentAcquisitionAttempt;
|
|
134
|
-
if (summary === null || summary.integrationPath !== "checkout_session") {
|
|
135
|
-
throw acquisitionBindingMismatch(session.id, "current_acquisition_attempt.integration_path", "The current Proxy acquisition is not a Checkout Session acquisition.");
|
|
136
|
-
}
|
|
137
|
-
const acquisitions = await options.proxy.providerAcquisitions.listProviderAcquisitions(session.id, { requestId: options.requestId });
|
|
138
|
-
const acquisition = acquisitions.find((candidate) => candidate.id === summary.id);
|
|
139
|
-
if (acquisition?.psp !== "stripe" ||
|
|
140
|
-
acquisition.integrationPath !== "checkout_session" ||
|
|
141
|
-
acquisition.providerRootObjectType !== "checkout_session" ||
|
|
142
|
-
acquisition.providerRootObjectId !== options.checkoutSessionId) {
|
|
143
|
-
throw acquisitionBindingMismatch(session.id, "current_acquisition_attempt", "The current Proxy acquisition is not bound to the requested Stripe Checkout Session.");
|
|
144
|
-
}
|
|
145
|
-
return acquisition;
|
|
146
|
-
}
|
|
147
|
-
function acquisitionBindingMismatch(proxySessionId, field, message) {
|
|
148
|
-
return new ProxyCheckoutValidationError(`${message} Proxy session: ${proxySessionId}.`, {
|
|
149
|
-
code: "provider_binding_mismatch",
|
|
150
|
-
field,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
83
|
function buildStripeUpdateRequestOptions(options, cartVersion) {
|
|
154
84
|
return {
|
|
155
85
|
...options.stripeRequestOptions,
|