@proxy-checkout/stripe-server-js 0.0.1 → 0.1.0-prx-124.139.1

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 CHANGED
@@ -4,7 +4,7 @@ Server-side Stripe adapter for Proxy Checkout. It owns the Stripe-specific integ
4
4
 
5
5
  - **`requiredMetadata(proxySession)`** — the `proxy_session_id` metadata Proxy needs on the Checkout Session, Subscription, and PaymentIntent.
6
6
  - **`assertCheckoutSessionBelongsToProxy` / `assertSubscriptionBelongsToProxy` / `assertPaymentIntentBelongsToProxy`** — provider-object ownership checks.
7
- - **`openCheckout(...)`** — records payer-opened, validates/reconciles the typed cart with rollback, injects required metadata, creates the Stripe Checkout Session with deterministic idempotency, records the Proxy provider binding, and returns the client secret. The merchant only supplies app-specific Stripe params.
7
+ - **`openCheckout(...)`** — records payer-opened, validates/reconciles the typed cart with rollback, injects required metadata, creates or joins the Stripe Checkout Session with deterministic idempotency, and records the Proxy provider binding. It returns provider-create material only for a `ready` outcome; terminal outcomes retain the shared cart without starting Stripe.
8
8
  - **`syncCheckoutCart(...)`** — validates the stored Proxy provider binding, then updates the Proxy cart and the Stripe Checkout `line_items` in the safest order with deterministic rollback/reconciliation (`ProxyStripeCartSyncError`).
9
9
 
10
10
  PSP-agnostic primitives (sessions, subscriptions, webhooks, lifecycle) live in [`@proxy-checkout/server-js`](https://www.npmjs.com/package/@proxy-checkout/server-js). This package is an additive adapter on top and is dependency-free: you inject your own `Stripe` instance and your `@proxy-checkout/server-js` client.
@@ -38,19 +38,23 @@ const checkout = await openCheckout({
38
38
  ui_mode: "custom",
39
39
  }),
40
40
  });
41
- // checkout.clientSecret -> render Stripe Elements
42
-
43
- // Cart edits go through the binding-validated sync helper.
44
- await syncCheckoutCart({
45
- proxy,
46
- stripe,
47
- proxySessionId,
48
- checkoutSessionId: checkout.checkoutSession.id,
49
- cartSchema,
50
- input: { interval: "year" },
51
- buildNextCart: ({ currentCart, input }) => buildMerchantCart(currentCart, input),
52
- buildStripeLineItems: (cart) => cart.lineItems,
53
- });
41
+ if (checkout.outcome === "ready") {
42
+ // checkout.clientSecret -> render Stripe Elements
43
+
44
+ // Cart edits go through the binding-validated sync helper.
45
+ await syncCheckoutCart({
46
+ proxy,
47
+ stripe,
48
+ proxySessionId,
49
+ checkoutSessionId: checkout.checkoutSession.id,
50
+ cartSchema,
51
+ input: { interval: "year" },
52
+ buildNextCart: ({ currentCart, input }) => buildMerchantCart(currentCart, input),
53
+ buildStripeLineItems: (cart) => cart.lineItems,
54
+ });
55
+ } else {
56
+ // Render checkout.cart plus checkout.outcome/sessionStatus. Do not start Stripe.
57
+ }
54
58
  ```
55
59
 
56
60
  Use the delegated-checkout guide from your Proxy onboarding materials for the
@@ -1,6 +1,6 @@
1
1
  export { type ProxyStripeCartSyncCode, ProxyStripeCartSyncError, type ProxyStripeOpenCheckoutCode, ProxyStripeOpenCheckoutError, } from "./errors.js";
2
- export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, PROXY_SESSION_METADATA_KEY, type ProxySessionMetadata, type ProxyStripeRequiredMetadata, requiredMetadata, } from "./metadata.js";
3
- export { type OpenCheckoutOptions, type OpenCheckoutReconciledCart, type OpenCheckoutResult, openCheckout, } from "./open-checkout.js";
2
+ export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, PROXY_INTEGRATION_PATH_METADATA_KEY, PROXY_SESSION_METADATA_KEY, type ProxySessionMetadata, type ProxyStripeRequiredMetadata, requiredMetadata, } from "./metadata.js";
3
+ export { type OpenCheckoutOptions, type OpenCheckoutReconciledCart, type OpenCheckoutResponse, type OpenCheckoutResult, type OpenCheckoutTerminalResult, openCheckout, } from "./open-checkout.js";
4
4
  export { normalizePsp } from "./psp.js";
5
5
  export { type SyncCheckoutCartOptions, type SyncCheckoutCartResult, type SyncedCart, syncCheckoutCart, } from "./sync-checkout-cart.js";
6
6
  export type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripeCheckoutSessionsApiLike, StripeCheckoutSessionUpdateParams, StripeClientLike, StripeMetadata, StripePaymentIntentLike, StripeRequestOptionsLike, StripeSubscriptionLike, } from "./types.js";
@@ -1,6 +1,6 @@
1
1
  export { type ProxyStripeCartSyncCode, ProxyStripeCartSyncError, type ProxyStripeOpenCheckoutCode, ProxyStripeOpenCheckoutError, } from "./errors.js";
2
- export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, PROXY_SESSION_METADATA_KEY, type ProxySessionMetadata, type ProxyStripeRequiredMetadata, requiredMetadata, } from "./metadata.js";
3
- export { type OpenCheckoutOptions, type OpenCheckoutReconciledCart, type OpenCheckoutResult, openCheckout, } from "./open-checkout.js";
2
+ export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, PROXY_INTEGRATION_PATH_METADATA_KEY, PROXY_SESSION_METADATA_KEY, type ProxySessionMetadata, type ProxyStripeRequiredMetadata, requiredMetadata, } from "./metadata.js";
3
+ export { type OpenCheckoutOptions, type OpenCheckoutReconciledCart, type OpenCheckoutResponse, type OpenCheckoutResult, type OpenCheckoutTerminalResult, openCheckout, } from "./open-checkout.js";
4
4
  export { normalizePsp } from "./psp.js";
5
5
  export { type SyncCheckoutCartOptions, type SyncCheckoutCartResult, type SyncedCart, syncCheckoutCart, } from "./sync-checkout-cart.js";
6
6
  export type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripeCheckoutSessionsApiLike, StripeCheckoutSessionUpdateParams, StripeClientLike, StripeMetadata, StripePaymentIntentLike, StripeRequestOptionsLike, StripeSubscriptionLike, } from "./types.js";
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.proxyCheckoutStripeServerSdkVersion = exports.proxyCheckoutStripeServerSdkName = exports.syncCheckoutCart = exports.normalizePsp = exports.openCheckout = exports.requiredMetadata = exports.PROXY_SESSION_METADATA_KEY = exports.injectRequiredCheckoutSessionMetadata = exports.assertSubscriptionBelongsToProxy = exports.assertPaymentIntentBelongsToProxy = exports.assertCheckoutSessionBelongsToProxy = exports.ProxyStripeOpenCheckoutError = exports.ProxyStripeCartSyncError = void 0;
3
+ exports.proxyCheckoutStripeServerSdkVersion = exports.proxyCheckoutStripeServerSdkName = exports.syncCheckoutCart = exports.normalizePsp = exports.openCheckout = exports.requiredMetadata = exports.PROXY_SESSION_METADATA_KEY = exports.PROXY_INTEGRATION_PATH_METADATA_KEY = exports.injectRequiredCheckoutSessionMetadata = exports.assertSubscriptionBelongsToProxy = exports.assertPaymentIntentBelongsToProxy = exports.assertCheckoutSessionBelongsToProxy = exports.ProxyStripeOpenCheckoutError = exports.ProxyStripeCartSyncError = void 0;
4
4
  var errors_js_1 = require("./errors.js");
5
5
  Object.defineProperty(exports, "ProxyStripeCartSyncError", { enumerable: true, get: function () { return errors_js_1.ProxyStripeCartSyncError; } });
6
6
  Object.defineProperty(exports, "ProxyStripeOpenCheckoutError", { enumerable: true, get: function () { return errors_js_1.ProxyStripeOpenCheckoutError; } });
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "assertCheckoutSessionBelongsToProxy", { enumerab
9
9
  Object.defineProperty(exports, "assertPaymentIntentBelongsToProxy", { enumerable: true, get: function () { return metadata_js_1.assertPaymentIntentBelongsToProxy; } });
10
10
  Object.defineProperty(exports, "assertSubscriptionBelongsToProxy", { enumerable: true, get: function () { return metadata_js_1.assertSubscriptionBelongsToProxy; } });
11
11
  Object.defineProperty(exports, "injectRequiredCheckoutSessionMetadata", { enumerable: true, get: function () { return metadata_js_1.injectRequiredCheckoutSessionMetadata; } });
12
+ Object.defineProperty(exports, "PROXY_INTEGRATION_PATH_METADATA_KEY", { enumerable: true, get: function () { return metadata_js_1.PROXY_INTEGRATION_PATH_METADATA_KEY; } });
12
13
  Object.defineProperty(exports, "PROXY_SESSION_METADATA_KEY", { enumerable: true, get: function () { return metadata_js_1.PROXY_SESSION_METADATA_KEY; } });
13
14
  Object.defineProperty(exports, "requiredMetadata", { enumerable: true, get: function () { return metadata_js_1.requiredMetadata; } });
14
15
  var open_checkout_js_1 = require("./open-checkout.js");
@@ -9,6 +9,8 @@
9
9
  import type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripePaymentIntentLike, StripeSubscriptionLike } from "./types.js";
10
10
  /** The single metadata key Proxy reads to correlate a Stripe object to a session. */
11
11
  export declare const PROXY_SESSION_METADATA_KEY = "proxy_session_id";
12
+ /** Server-owned marker that distinguishes Checkout-created child objects from direct objects. */
13
+ export declare const PROXY_INTEGRATION_PATH_METADATA_KEY = "proxy_integration_path";
12
14
  export type ProxySessionMetadata = Readonly<Record<"proxy_session_id", string>>;
13
15
  /**
14
16
  * The metadata each Stripe object must carry, ready to spread into Stripe params.
@@ -9,6 +9,8 @@
9
9
  import type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripePaymentIntentLike, StripeSubscriptionLike } from "./types.js";
10
10
  /** The single metadata key Proxy reads to correlate a Stripe object to a session. */
11
11
  export declare const PROXY_SESSION_METADATA_KEY = "proxy_session_id";
12
+ /** Server-owned marker that distinguishes Checkout-created child objects from direct objects. */
13
+ export declare const PROXY_INTEGRATION_PATH_METADATA_KEY = "proxy_integration_path";
12
14
  export type ProxySessionMetadata = Readonly<Record<"proxy_session_id", string>>;
13
15
  /**
14
16
  * The metadata each Stripe object must carry, ready to spread into Stripe params.
@@ -8,7 +8,7 @@
8
8
  * provides assertions that a Stripe object actually belongs to a Proxy session.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.PROXY_SESSION_METADATA_KEY = void 0;
11
+ exports.PROXY_INTEGRATION_PATH_METADATA_KEY = exports.PROXY_SESSION_METADATA_KEY = void 0;
12
12
  exports.requiredMetadata = requiredMetadata;
13
13
  exports.injectRequiredCheckoutSessionMetadata = injectRequiredCheckoutSessionMetadata;
14
14
  exports.assertCheckoutSessionBelongsToProxy = assertCheckoutSessionBelongsToProxy;
@@ -17,6 +17,8 @@ exports.assertPaymentIntentBelongsToProxy = assertPaymentIntentBelongsToProxy;
17
17
  const server_js_1 = require("@proxy-checkout/server-js");
18
18
  /** The single metadata key Proxy reads to correlate a Stripe object to a session. */
19
19
  exports.PROXY_SESSION_METADATA_KEY = "proxy_session_id";
20
+ /** Server-owned marker that distinguishes Checkout-created child objects from direct objects. */
21
+ exports.PROXY_INTEGRATION_PATH_METADATA_KEY = "proxy_integration_path";
20
22
  /** Build the required Stripe metadata placements for a Proxy session. */
21
23
  function requiredMetadata(proxySession) {
22
24
  const metadata = { proxy_session_id: proxySession.id };
@@ -35,14 +37,18 @@ function requiredMetadata(proxySession) {
35
37
  */
36
38
  function injectRequiredCheckoutSessionMetadata(params, proxySessionId) {
37
39
  const metadata = { proxy_session_id: proxySessionId };
40
+ const checkoutMetadata = {
41
+ ...metadata,
42
+ [exports.PROXY_INTEGRATION_PATH_METADATA_KEY]: "checkout_session",
43
+ };
38
44
  return {
39
45
  ...params,
40
- metadata: mergeProxyMetadata(params.metadata, metadata, "metadata.proxy_session_id"),
46
+ metadata: mergeProxyMetadata(params.metadata, checkoutMetadata, "metadata"),
41
47
  ...(params.mode === "subscription" || params.subscription_data !== undefined
42
48
  ? {
43
49
  subscription_data: {
44
50
  ...objectRecord(params.subscription_data),
45
- metadata: mergeProxyMetadata(metadataFromNestedParams(params.subscription_data), metadata, "subscription_data.metadata.proxy_session_id"),
51
+ metadata: mergeProxyMetadata(metadataFromNestedParams(params.subscription_data), checkoutMetadata, "subscription_data.metadata"),
46
52
  },
47
53
  }
48
54
  : {}),
@@ -50,7 +56,7 @@ function injectRequiredCheckoutSessionMetadata(params, proxySessionId) {
50
56
  ? {
51
57
  payment_intent_data: {
52
58
  ...objectRecord(params.payment_intent_data),
53
- metadata: mergeProxyMetadata(metadataFromNestedParams(params.payment_intent_data), metadata, "payment_intent_data.metadata.proxy_session_id"),
59
+ metadata: mergeProxyMetadata(metadataFromNestedParams(params.payment_intent_data), checkoutMetadata, "payment_intent_data.metadata"),
54
60
  },
55
61
  }
56
62
  : {}),
@@ -75,9 +81,11 @@ function assertProxyMetadata(metadata, proxySessionId, objectLabel) {
75
81
  }
76
82
  }
77
83
  function mergeProxyMetadata(existing, required, field) {
78
- const existingValue = existing?.[exports.PROXY_SESSION_METADATA_KEY];
79
- if (existingValue !== undefined && existingValue !== required.proxy_session_id) {
80
- throw new server_js_1.ProxyCheckoutValidationError(`Stripe metadata.${exports.PROXY_SESSION_METADATA_KEY} (${existingValue}) conflicts with Proxy session ${required.proxy_session_id}.`, { code: "provider_binding_mismatch", field });
84
+ for (const [key, requiredValue] of Object.entries(required)) {
85
+ const existingValue = existing?.[key];
86
+ if (existingValue !== undefined && existingValue !== requiredValue) {
87
+ throw new server_js_1.ProxyCheckoutValidationError(`Stripe ${field}.${key} (${existingValue}) conflicts with Proxy-required value ${requiredValue}.`, { code: "provider_binding_mismatch", field: `${field}.${key}` });
88
+ }
81
89
  }
82
90
  return {
83
91
  ...existing,
@@ -54,7 +54,28 @@ export interface OpenCheckoutResult<TCart, TOffer, TSession> {
54
54
  readonly checkoutSession: TSession;
55
55
  readonly clientSecret: string | null;
56
56
  readonly offer: TOffer;
57
+ readonly outcome: "ready";
57
58
  readonly proxySession: PayerOpenedResult;
58
59
  readonly proxySessionId: string;
59
60
  }
60
- export declare function openCheckout<TStripe extends StripeClientLike, TCart = unknown, TOffer = undefined, TSession extends StripeCheckoutSessionLike = StripeCheckoutSessionLike>(options: OpenCheckoutOptions<TStripe, TCart, TOffer>): Promise<OpenCheckoutResult<TCart, TOffer, TSession>>;
61
+ export type OpenCheckoutTerminalResult<TCart> = {
62
+ readonly cart: TCart;
63
+ readonly outcome: "already_paid";
64
+ readonly proxySession: PayerOpenedResult;
65
+ readonly proxySessionId: string;
66
+ readonly sessionStatus: "paid" | "provisionable" | "provisioned" | "provisioning_failed";
67
+ } | {
68
+ readonly cart: TCart;
69
+ readonly outcome: "action_required";
70
+ readonly proxySession: PayerOpenedResult;
71
+ readonly proxySessionId: string;
72
+ readonly sessionStatus: "merchant_action_required";
73
+ } | {
74
+ readonly cart: TCart;
75
+ readonly outcome: "unavailable";
76
+ readonly proxySession: PayerOpenedResult;
77
+ readonly proxySessionId: string;
78
+ readonly sessionStatus: "cancelled" | "created" | "expired" | "failed";
79
+ };
80
+ export type OpenCheckoutResponse<TCart, TOffer, TSession> = OpenCheckoutResult<TCart, TOffer, TSession> | OpenCheckoutTerminalResult<TCart>;
81
+ export declare function openCheckout<TStripe extends StripeClientLike, TCart = unknown, TOffer = undefined, TSession extends StripeCheckoutSessionLike = StripeCheckoutSessionLike>(options: OpenCheckoutOptions<TStripe, TCart, TOffer>): Promise<OpenCheckoutResponse<TCart, TOffer, TSession>>;
@@ -54,7 +54,28 @@ export interface OpenCheckoutResult<TCart, TOffer, TSession> {
54
54
  readonly checkoutSession: TSession;
55
55
  readonly clientSecret: string | null;
56
56
  readonly offer: TOffer;
57
+ readonly outcome: "ready";
57
58
  readonly proxySession: PayerOpenedResult;
58
59
  readonly proxySessionId: string;
59
60
  }
60
- export declare function openCheckout<TStripe extends StripeClientLike, TCart = unknown, TOffer = undefined, TSession extends StripeCheckoutSessionLike = StripeCheckoutSessionLike>(options: OpenCheckoutOptions<TStripe, TCart, TOffer>): Promise<OpenCheckoutResult<TCart, TOffer, TSession>>;
61
+ export type OpenCheckoutTerminalResult<TCart> = {
62
+ readonly cart: TCart;
63
+ readonly outcome: "already_paid";
64
+ readonly proxySession: PayerOpenedResult;
65
+ readonly proxySessionId: string;
66
+ readonly sessionStatus: "paid" | "provisionable" | "provisioned" | "provisioning_failed";
67
+ } | {
68
+ readonly cart: TCart;
69
+ readonly outcome: "action_required";
70
+ readonly proxySession: PayerOpenedResult;
71
+ readonly proxySessionId: string;
72
+ readonly sessionStatus: "merchant_action_required";
73
+ } | {
74
+ readonly cart: TCart;
75
+ readonly outcome: "unavailable";
76
+ readonly proxySession: PayerOpenedResult;
77
+ readonly proxySessionId: string;
78
+ readonly sessionStatus: "cancelled" | "created" | "expired" | "failed";
79
+ };
80
+ export type OpenCheckoutResponse<TCart, TOffer, TSession> = OpenCheckoutResult<TCart, TOffer, TSession> | OpenCheckoutTerminalResult<TCart>;
81
+ export declare function openCheckout<TStripe extends StripeClientLike, TCart = unknown, TOffer = undefined, TSession extends StripeCheckoutSessionLike = StripeCheckoutSessionLike>(options: OpenCheckoutOptions<TStripe, TCart, TOffer>): Promise<OpenCheckoutResponse<TCart, TOffer, TSession>>;
@@ -27,6 +27,13 @@ async function openCheckout(options) {
27
27
  let cart = options.cartSchema || options.getCartBuyerReference
28
28
  ? options.proxy.sessions.parseCart(proxySession, options.cartSchema ?? ((value) => value), { getBuyerReference: options.getCartBuyerReference })
29
29
  : proxySession.cartSnapshot;
30
+ if (proxySession.presentationState !== "checkout_available") {
31
+ return terminalResultFromSession(proxySession, cart);
32
+ }
33
+ if (proxySession.status === "payment_pending" &&
34
+ proxySession.providerCheckoutSessionId === null) {
35
+ throw new server_js_1.ProxyCheckoutValidationError(`Proxy session ${proxySession.id} already has payment preparation in progress without a bound Stripe Checkout Session.`, { code: "provider_binding_missing", field: "provider_checkout_session_id" });
36
+ }
30
37
  let offer = options.validateCart
31
38
  ? await options.validateCart({ cart, session: proxySession })
32
39
  : undefined;
@@ -40,10 +47,14 @@ async function openCheckout(options) {
40
47
  checkoutSession,
41
48
  clientSecret: checkoutSession.client_secret ?? null,
42
49
  offer,
50
+ outcome: "ready",
43
51
  proxySession,
44
52
  proxySessionId: proxySession.id,
45
53
  };
46
54
  }
55
+ if (proxySession.currentAcquisitionAttempt !== null) {
56
+ throw new server_js_1.ProxyCheckoutValidationError(`Proxy session ${proxySession.id} already has acquisition ${proxySession.currentAcquisitionAttempt.id} in progress; openCheckout will not create an unrelated Stripe Checkout Session.`, { code: "provider_acquisition_in_progress", field: "current_acquisition_attempt" });
57
+ }
47
58
  let rollbackCart;
48
59
  const reconciledCart = await options.reconcileCart?.({ cart, offer, session: proxySession });
49
60
  if (reconciledCart) {
@@ -56,42 +67,91 @@ async function openCheckout(options) {
56
67
  cartVersion: proxySession.cartVersion,
57
68
  currency: proxySession.currency,
58
69
  };
59
- const updatedCart = await options.proxy.sessions.cart.set(proxySession.id, {
60
- amountMinor: reconciledCart.amountMinor,
61
- cartSnapshot: reconciledCart.cartSnapshot,
62
- currency: reconciledCart.currency,
63
- expectedCartVersion: proxySession.cartVersion,
64
- requestId: options.requestId,
65
- });
66
- rollbackCart = {
67
- ...priorCart,
68
- expectedCartVersion: updatedCart.cartVersion,
69
- };
70
- proxySession = {
71
- ...proxySession,
72
- amountMinor: updatedCart.amountMinor,
73
- cartSnapshot: updatedCart.cartSnapshot,
74
- cartVersion: updatedCart.cartVersion,
75
- currency: updatedCart.currency,
76
- updatedAt: proxySession.updatedAt,
77
- };
70
+ try {
71
+ const updatedCart = await options.proxy.sessions.cart.set(proxySession.id, {
72
+ amountMinor: reconciledCart.amountMinor,
73
+ cartSnapshot: reconciledCart.cartSnapshot,
74
+ currency: reconciledCart.currency,
75
+ expectedCartVersion: proxySession.cartVersion,
76
+ requestId: options.requestId,
77
+ });
78
+ rollbackCart = {
79
+ ...priorCart,
80
+ expectedCartVersion: updatedCart.cartVersion,
81
+ };
82
+ proxySession = {
83
+ ...proxySession,
84
+ amountMinor: updatedCart.amountMinor,
85
+ cartSnapshot: updatedCart.cartSnapshot,
86
+ cartVersion: updatedCart.cartVersion,
87
+ currency: updatedCart.currency,
88
+ updatedAt: proxySession.updatedAt,
89
+ };
90
+ }
91
+ catch (cause) {
92
+ if (!isProxyConflict(cause)) {
93
+ throw cause;
94
+ }
95
+ const joined = await options.proxy.sessions.payerOpened(proxySession.id, {
96
+ requestId: options.requestId,
97
+ });
98
+ if (!sameCheckoutCart(joined, reconciledCart)) {
99
+ throw cause;
100
+ }
101
+ proxySession = joined;
102
+ }
78
103
  cart = reconciledCart.cart;
79
104
  }
80
105
  try {
81
106
  if (reconciledCart && options.validateCart) {
82
107
  offer = await options.validateCart({ cart, session: proxySession });
83
108
  }
109
+ const current = await options.proxy.sessions.payerOpened(proxySession.id, {
110
+ requestId: options.requestId,
111
+ });
112
+ if (current.presentationState !== "checkout_available") {
113
+ return terminalResultFromSession(current, cart);
114
+ }
115
+ if (!sameCheckoutCart(current, proxySession)) {
116
+ throw new server_js_1.ProxyCheckoutValidationError(`Proxy session ${proxySession.id} cart changed while Checkout was being prepared.`, { code: "cart_version_conflict", field: "cart_version" });
117
+ }
118
+ proxySession = current;
119
+ if (proxySession.providerCheckoutSessionId !== null) {
120
+ assertExistingBindingMatches(proxySession, psp);
121
+ const checkoutSession = (await options.stripe.checkout.sessions.retrieve(proxySession.providerCheckoutSessionId, undefined, options.stripeRequestOptions));
122
+ (0, metadata_js_1.assertCheckoutSessionBelongsToProxy)(checkoutSession, proxySession.id);
123
+ return {
124
+ binding: bindingFromSession(proxySession),
125
+ cart,
126
+ checkoutSession,
127
+ clientSecret: checkoutSession.client_secret ?? null,
128
+ offer,
129
+ outcome: "ready",
130
+ proxySession,
131
+ proxySessionId: proxySession.id,
132
+ };
133
+ }
134
+ if (proxySession.currentAcquisitionAttempt !== null) {
135
+ // A competing acquisition now owns the reconciled cart. Do not roll it
136
+ // back underneath that owner.
137
+ rollbackCart = undefined;
138
+ throw new server_js_1.ProxyCheckoutValidationError(`Proxy session ${proxySession.id} already has acquisition ${proxySession.currentAcquisitionAttempt.id} in progress; openCheckout will not create an unrelated Stripe Checkout Session.`, { code: "provider_acquisition_in_progress", field: "current_acquisition_attempt" });
139
+ }
84
140
  const params = await options.buildCheckoutSessionParams({
85
141
  cart,
86
142
  offer,
87
143
  proxySession,
88
144
  stripe: options.stripe,
89
145
  });
146
+ assertCheckoutAutomaticRecoveryDisabled(params, proxySession.id);
147
+ const commercialMode = checkoutCommercialMode(params, proxySession.id);
90
148
  const checkoutSession = (await options.stripe.checkout.sessions.create((0, metadata_js_1.injectRequiredCheckoutSessionMetadata)(params, proxySession.id), buildStripeRequestOptions(options, proxySession.id, psp)));
91
149
  // Fail closed if the merchant forgot to place proxy_session_id metadata.
92
150
  (0, metadata_js_1.assertCheckoutSessionBelongsToProxy)(checkoutSession, proxySession.id);
93
151
  const binding = await recordOrReconcileProviderBinding({
94
152
  checkoutSessionId: checkoutSession.id,
153
+ expectedCartVersion: proxySession.cartVersion,
154
+ commercialMode,
95
155
  proxy: options.proxy,
96
156
  proxySessionId: proxySession.id,
97
157
  psp,
@@ -105,6 +165,7 @@ async function openCheckout(options) {
105
165
  checkoutSession,
106
166
  clientSecret: checkoutSession.client_secret ?? null,
107
167
  offer,
168
+ outcome: "ready",
108
169
  proxySession,
109
170
  proxySessionId: proxySession.id,
110
171
  };
@@ -122,6 +183,39 @@ async function openCheckout(options) {
122
183
  throw cause;
123
184
  }
124
185
  }
186
+ function terminalResultFromSession(proxySession, cart) {
187
+ const common = {
188
+ cart,
189
+ proxySession,
190
+ proxySessionId: proxySession.id,
191
+ };
192
+ switch (proxySession.presentationState) {
193
+ case "already_paid":
194
+ if (proxySession.status !== "paid" &&
195
+ proxySession.status !== "provisionable" &&
196
+ proxySession.status !== "provisioned" &&
197
+ proxySession.status !== "provisioning_failed") {
198
+ break;
199
+ }
200
+ return { ...common, outcome: "already_paid", sessionStatus: proxySession.status };
201
+ case "action_required":
202
+ if (proxySession.status !== "merchant_action_required") {
203
+ break;
204
+ }
205
+ return { ...common, outcome: "action_required", sessionStatus: proxySession.status };
206
+ case "unavailable":
207
+ if (proxySession.status !== "cancelled" &&
208
+ proxySession.status !== "created" &&
209
+ proxySession.status !== "expired" &&
210
+ proxySession.status !== "failed") {
211
+ break;
212
+ }
213
+ return { ...common, outcome: "unavailable", sessionStatus: proxySession.status };
214
+ case "checkout_available":
215
+ break;
216
+ }
217
+ throw new server_js_1.ProxyCheckoutValidationError(`Proxy session ${proxySession.id} returned an inconsistent presentation and lifecycle status.`, { code: "invalid_session_presentation", field: "presentation_state" });
218
+ }
125
219
  function buildStripeRequestOptions(options, proxySessionId, psp) {
126
220
  return {
127
221
  ...options.stripeRequestOptions,
@@ -170,6 +264,8 @@ async function rollbackReconciledCart(input) {
170
264
  async function recordOrReconcileProviderBinding(input) {
171
265
  try {
172
266
  return await input.proxy.sessions.recordProviderBinding(input.proxySessionId, {
267
+ commercialMode: input.commercialMode,
268
+ expectedCartVersion: input.expectedCartVersion,
173
269
  providerCheckoutSessionId: input.checkoutSessionId,
174
270
  psp: input.psp,
175
271
  requestId: input.requestId,
@@ -190,6 +286,37 @@ async function recordOrReconcileProviderBinding(input) {
190
286
  });
191
287
  }
192
288
  }
289
+ function sameCheckoutCart(actual, expected) {
290
+ return (actual.amountMinor === expected.amountMinor &&
291
+ actual.currency === expected.currency &&
292
+ canonicalJson(actual.cartSnapshot) === canonicalJson(expected.cartSnapshot));
293
+ }
294
+ function canonicalJson(value) {
295
+ if (Array.isArray(value)) {
296
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
297
+ }
298
+ if (typeof value === "object" && value !== null) {
299
+ return `{${Object.entries(value)
300
+ .sort(([left], [right]) => left.localeCompare(right))
301
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
302
+ .join(",")}}`;
303
+ }
304
+ return JSON.stringify(value);
305
+ }
306
+ function checkoutCommercialMode(params, proxySessionId) {
307
+ if (params.mode === "payment") {
308
+ return "one_time";
309
+ }
310
+ if (params.mode === "subscription") {
311
+ return "subscription";
312
+ }
313
+ throw new server_js_1.ProxyCheckoutValidationError(`Proxy session ${proxySessionId} requires Stripe Checkout mode payment or subscription.`, { code: "unsupported_provider_option", field: "mode" });
314
+ }
315
+ function assertCheckoutAutomaticRecoveryDisabled(params, proxySessionId) {
316
+ if (params.after_expiration?.recovery?.enabled === true) {
317
+ throw new server_js_1.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" });
318
+ }
319
+ }
193
320
  async function readReconciledBinding(input) {
194
321
  const current = await input.proxy.sessions.retrieve(input.proxySessionId, {
195
322
  requestId: input.requestId,
@@ -27,6 +27,11 @@ export interface StripeCheckoutSessionUpdateParams {
27
27
  readonly metadata?: StripeMetadataParam;
28
28
  }
29
29
  export type StripeCheckoutSessionCreateParams = object & {
30
+ readonly after_expiration?: {
31
+ readonly recovery?: {
32
+ readonly enabled?: boolean;
33
+ };
34
+ };
30
35
  readonly metadata?: StripeMetadataParam;
31
36
  readonly mode?: "payment" | "setup" | "subscription";
32
37
  readonly payment_intent_data?: unknown;
@@ -27,6 +27,11 @@ export interface StripeCheckoutSessionUpdateParams {
27
27
  readonly metadata?: StripeMetadataParam;
28
28
  }
29
29
  export type StripeCheckoutSessionCreateParams = object & {
30
+ readonly after_expiration?: {
31
+ readonly recovery?: {
32
+ readonly enabled?: boolean;
33
+ };
34
+ };
30
35
  readonly metadata?: StripeMetadataParam;
31
36
  readonly mode?: "payment" | "setup" | "subscription";
32
37
  readonly payment_intent_data?: unknown;
@@ -1,2 +1,2 @@
1
1
  export declare const proxyCheckoutStripeServerSdkName = "@proxy-checkout/stripe-server-js";
2
- export declare const proxyCheckoutStripeServerSdkVersion = "0.0.1";
2
+ export declare const proxyCheckoutStripeServerSdkVersion = "0.1.0-prx-124.139.1";
@@ -1,2 +1,2 @@
1
1
  export declare const proxyCheckoutStripeServerSdkName = "@proxy-checkout/stripe-server-js";
2
- export declare const proxyCheckoutStripeServerSdkVersion = "0.0.1";
2
+ export declare const proxyCheckoutStripeServerSdkVersion = "0.1.0-prx-124.139.1";
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.proxyCheckoutStripeServerSdkVersion = exports.proxyCheckoutStripeServerSdkName = void 0;
4
4
  exports.proxyCheckoutStripeServerSdkName = "@proxy-checkout/stripe-server-js";
5
- exports.proxyCheckoutStripeServerSdkVersion = "0.0.1";
5
+ exports.proxyCheckoutStripeServerSdkVersion = "0.1.0-prx-124.139.1";
@@ -1,6 +1,6 @@
1
1
  export { type ProxyStripeCartSyncCode, ProxyStripeCartSyncError, type ProxyStripeOpenCheckoutCode, ProxyStripeOpenCheckoutError, } from "./errors.js";
2
- export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, PROXY_SESSION_METADATA_KEY, type ProxySessionMetadata, type ProxyStripeRequiredMetadata, requiredMetadata, } from "./metadata.js";
3
- export { type OpenCheckoutOptions, type OpenCheckoutReconciledCart, type OpenCheckoutResult, openCheckout, } from "./open-checkout.js";
2
+ export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, PROXY_INTEGRATION_PATH_METADATA_KEY, PROXY_SESSION_METADATA_KEY, type ProxySessionMetadata, type ProxyStripeRequiredMetadata, requiredMetadata, } from "./metadata.js";
3
+ export { type OpenCheckoutOptions, type OpenCheckoutReconciledCart, type OpenCheckoutResponse, type OpenCheckoutResult, type OpenCheckoutTerminalResult, openCheckout, } from "./open-checkout.js";
4
4
  export { normalizePsp } from "./psp.js";
5
5
  export { type SyncCheckoutCartOptions, type SyncCheckoutCartResult, type SyncedCart, syncCheckoutCart, } from "./sync-checkout-cart.js";
6
6
  export type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripeCheckoutSessionsApiLike, StripeCheckoutSessionUpdateParams, StripeClientLike, StripeMetadata, StripePaymentIntentLike, StripeRequestOptionsLike, StripeSubscriptionLike, } from "./types.js";
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ProxyStripeCartSyncError, ProxyStripeOpenCheckoutError, } from "./errors.js";
2
- export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, PROXY_SESSION_METADATA_KEY, requiredMetadata, } from "./metadata.js";
2
+ export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, PROXY_INTEGRATION_PATH_METADATA_KEY, PROXY_SESSION_METADATA_KEY, requiredMetadata, } from "./metadata.js";
3
3
  export { openCheckout, } from "./open-checkout.js";
4
4
  export { normalizePsp } from "./psp.js";
5
5
  export { syncCheckoutCart, } from "./sync-checkout-cart.js";
@@ -9,6 +9,8 @@
9
9
  import type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripePaymentIntentLike, StripeSubscriptionLike } from "./types.js";
10
10
  /** The single metadata key Proxy reads to correlate a Stripe object to a session. */
11
11
  export declare const PROXY_SESSION_METADATA_KEY = "proxy_session_id";
12
+ /** Server-owned marker that distinguishes Checkout-created child objects from direct objects. */
13
+ export declare const PROXY_INTEGRATION_PATH_METADATA_KEY = "proxy_integration_path";
12
14
  export type ProxySessionMetadata = Readonly<Record<"proxy_session_id", string>>;
13
15
  /**
14
16
  * The metadata each Stripe object must carry, ready to spread into Stripe params.
@@ -9,6 +9,8 @@
9
9
  import { ProxyCheckoutValidationError } from "@proxy-checkout/server-js";
10
10
  /** The single metadata key Proxy reads to correlate a Stripe object to a session. */
11
11
  export const PROXY_SESSION_METADATA_KEY = "proxy_session_id";
12
+ /** Server-owned marker that distinguishes Checkout-created child objects from direct objects. */
13
+ export const PROXY_INTEGRATION_PATH_METADATA_KEY = "proxy_integration_path";
12
14
  /** Build the required Stripe metadata placements for a Proxy session. */
13
15
  export function requiredMetadata(proxySession) {
14
16
  const metadata = { proxy_session_id: proxySession.id };
@@ -27,14 +29,18 @@ export function requiredMetadata(proxySession) {
27
29
  */
28
30
  export function injectRequiredCheckoutSessionMetadata(params, proxySessionId) {
29
31
  const metadata = { proxy_session_id: proxySessionId };
32
+ const checkoutMetadata = {
33
+ ...metadata,
34
+ [PROXY_INTEGRATION_PATH_METADATA_KEY]: "checkout_session",
35
+ };
30
36
  return {
31
37
  ...params,
32
- metadata: mergeProxyMetadata(params.metadata, metadata, "metadata.proxy_session_id"),
38
+ metadata: mergeProxyMetadata(params.metadata, checkoutMetadata, "metadata"),
33
39
  ...(params.mode === "subscription" || params.subscription_data !== undefined
34
40
  ? {
35
41
  subscription_data: {
36
42
  ...objectRecord(params.subscription_data),
37
- metadata: mergeProxyMetadata(metadataFromNestedParams(params.subscription_data), metadata, "subscription_data.metadata.proxy_session_id"),
43
+ metadata: mergeProxyMetadata(metadataFromNestedParams(params.subscription_data), checkoutMetadata, "subscription_data.metadata"),
38
44
  },
39
45
  }
40
46
  : {}),
@@ -42,7 +48,7 @@ export function injectRequiredCheckoutSessionMetadata(params, proxySessionId) {
42
48
  ? {
43
49
  payment_intent_data: {
44
50
  ...objectRecord(params.payment_intent_data),
45
- metadata: mergeProxyMetadata(metadataFromNestedParams(params.payment_intent_data), metadata, "payment_intent_data.metadata.proxy_session_id"),
51
+ metadata: mergeProxyMetadata(metadataFromNestedParams(params.payment_intent_data), checkoutMetadata, "payment_intent_data.metadata"),
46
52
  },
47
53
  }
48
54
  : {}),
@@ -67,9 +73,11 @@ function assertProxyMetadata(metadata, proxySessionId, objectLabel) {
67
73
  }
68
74
  }
69
75
  function mergeProxyMetadata(existing, required, field) {
70
- const existingValue = existing?.[PROXY_SESSION_METADATA_KEY];
71
- if (existingValue !== undefined && existingValue !== required.proxy_session_id) {
72
- throw new ProxyCheckoutValidationError(`Stripe metadata.${PROXY_SESSION_METADATA_KEY} (${existingValue}) conflicts with Proxy session ${required.proxy_session_id}.`, { code: "provider_binding_mismatch", field });
76
+ for (const [key, requiredValue] of Object.entries(required)) {
77
+ const existingValue = existing?.[key];
78
+ if (existingValue !== undefined && existingValue !== requiredValue) {
79
+ throw new ProxyCheckoutValidationError(`Stripe ${field}.${key} (${existingValue}) conflicts with Proxy-required value ${requiredValue}.`, { code: "provider_binding_mismatch", field: `${field}.${key}` });
80
+ }
73
81
  }
74
82
  return {
75
83
  ...existing,
@@ -54,7 +54,28 @@ export interface OpenCheckoutResult<TCart, TOffer, TSession> {
54
54
  readonly checkoutSession: TSession;
55
55
  readonly clientSecret: string | null;
56
56
  readonly offer: TOffer;
57
+ readonly outcome: "ready";
57
58
  readonly proxySession: PayerOpenedResult;
58
59
  readonly proxySessionId: string;
59
60
  }
60
- export declare function openCheckout<TStripe extends StripeClientLike, TCart = unknown, TOffer = undefined, TSession extends StripeCheckoutSessionLike = StripeCheckoutSessionLike>(options: OpenCheckoutOptions<TStripe, TCart, TOffer>): Promise<OpenCheckoutResult<TCart, TOffer, TSession>>;
61
+ export type OpenCheckoutTerminalResult<TCart> = {
62
+ readonly cart: TCart;
63
+ readonly outcome: "already_paid";
64
+ readonly proxySession: PayerOpenedResult;
65
+ readonly proxySessionId: string;
66
+ readonly sessionStatus: "paid" | "provisionable" | "provisioned" | "provisioning_failed";
67
+ } | {
68
+ readonly cart: TCart;
69
+ readonly outcome: "action_required";
70
+ readonly proxySession: PayerOpenedResult;
71
+ readonly proxySessionId: string;
72
+ readonly sessionStatus: "merchant_action_required";
73
+ } | {
74
+ readonly cart: TCart;
75
+ readonly outcome: "unavailable";
76
+ readonly proxySession: PayerOpenedResult;
77
+ readonly proxySessionId: string;
78
+ readonly sessionStatus: "cancelled" | "created" | "expired" | "failed";
79
+ };
80
+ export type OpenCheckoutResponse<TCart, TOffer, TSession> = OpenCheckoutResult<TCart, TOffer, TSession> | OpenCheckoutTerminalResult<TCart>;
81
+ export declare function openCheckout<TStripe extends StripeClientLike, TCart = unknown, TOffer = undefined, TSession extends StripeCheckoutSessionLike = StripeCheckoutSessionLike>(options: OpenCheckoutOptions<TStripe, TCart, TOffer>): Promise<OpenCheckoutResponse<TCart, TOffer, TSession>>;
@@ -24,6 +24,13 @@ export async function openCheckout(options) {
24
24
  let cart = options.cartSchema || options.getCartBuyerReference
25
25
  ? options.proxy.sessions.parseCart(proxySession, options.cartSchema ?? ((value) => value), { getBuyerReference: options.getCartBuyerReference })
26
26
  : proxySession.cartSnapshot;
27
+ if (proxySession.presentationState !== "checkout_available") {
28
+ return terminalResultFromSession(proxySession, cart);
29
+ }
30
+ if (proxySession.status === "payment_pending" &&
31
+ proxySession.providerCheckoutSessionId === null) {
32
+ throw new ProxyCheckoutValidationError(`Proxy session ${proxySession.id} already has payment preparation in progress without a bound Stripe Checkout Session.`, { code: "provider_binding_missing", field: "provider_checkout_session_id" });
33
+ }
27
34
  let offer = options.validateCart
28
35
  ? await options.validateCart({ cart, session: proxySession })
29
36
  : undefined;
@@ -37,10 +44,14 @@ export async function openCheckout(options) {
37
44
  checkoutSession,
38
45
  clientSecret: checkoutSession.client_secret ?? null,
39
46
  offer,
47
+ outcome: "ready",
40
48
  proxySession,
41
49
  proxySessionId: proxySession.id,
42
50
  };
43
51
  }
52
+ if (proxySession.currentAcquisitionAttempt !== null) {
53
+ throw new ProxyCheckoutValidationError(`Proxy session ${proxySession.id} already has acquisition ${proxySession.currentAcquisitionAttempt.id} in progress; openCheckout will not create an unrelated Stripe Checkout Session.`, { code: "provider_acquisition_in_progress", field: "current_acquisition_attempt" });
54
+ }
44
55
  let rollbackCart;
45
56
  const reconciledCart = await options.reconcileCart?.({ cart, offer, session: proxySession });
46
57
  if (reconciledCart) {
@@ -53,42 +64,91 @@ export async function openCheckout(options) {
53
64
  cartVersion: proxySession.cartVersion,
54
65
  currency: proxySession.currency,
55
66
  };
56
- const updatedCart = await options.proxy.sessions.cart.set(proxySession.id, {
57
- amountMinor: reconciledCart.amountMinor,
58
- cartSnapshot: reconciledCart.cartSnapshot,
59
- currency: reconciledCart.currency,
60
- expectedCartVersion: proxySession.cartVersion,
61
- requestId: options.requestId,
62
- });
63
- rollbackCart = {
64
- ...priorCart,
65
- expectedCartVersion: updatedCart.cartVersion,
66
- };
67
- proxySession = {
68
- ...proxySession,
69
- amountMinor: updatedCart.amountMinor,
70
- cartSnapshot: updatedCart.cartSnapshot,
71
- cartVersion: updatedCart.cartVersion,
72
- currency: updatedCart.currency,
73
- updatedAt: proxySession.updatedAt,
74
- };
67
+ try {
68
+ const updatedCart = await options.proxy.sessions.cart.set(proxySession.id, {
69
+ amountMinor: reconciledCart.amountMinor,
70
+ cartSnapshot: reconciledCart.cartSnapshot,
71
+ currency: reconciledCart.currency,
72
+ expectedCartVersion: proxySession.cartVersion,
73
+ requestId: options.requestId,
74
+ });
75
+ rollbackCart = {
76
+ ...priorCart,
77
+ expectedCartVersion: updatedCart.cartVersion,
78
+ };
79
+ proxySession = {
80
+ ...proxySession,
81
+ amountMinor: updatedCart.amountMinor,
82
+ cartSnapshot: updatedCart.cartSnapshot,
83
+ cartVersion: updatedCart.cartVersion,
84
+ currency: updatedCart.currency,
85
+ updatedAt: proxySession.updatedAt,
86
+ };
87
+ }
88
+ catch (cause) {
89
+ if (!isProxyConflict(cause)) {
90
+ throw cause;
91
+ }
92
+ const joined = await options.proxy.sessions.payerOpened(proxySession.id, {
93
+ requestId: options.requestId,
94
+ });
95
+ if (!sameCheckoutCart(joined, reconciledCart)) {
96
+ throw cause;
97
+ }
98
+ proxySession = joined;
99
+ }
75
100
  cart = reconciledCart.cart;
76
101
  }
77
102
  try {
78
103
  if (reconciledCart && options.validateCart) {
79
104
  offer = await options.validateCart({ cart, session: proxySession });
80
105
  }
106
+ const current = await options.proxy.sessions.payerOpened(proxySession.id, {
107
+ requestId: options.requestId,
108
+ });
109
+ if (current.presentationState !== "checkout_available") {
110
+ return terminalResultFromSession(current, cart);
111
+ }
112
+ if (!sameCheckoutCart(current, proxySession)) {
113
+ throw new ProxyCheckoutValidationError(`Proxy session ${proxySession.id} cart changed while Checkout was being prepared.`, { code: "cart_version_conflict", field: "cart_version" });
114
+ }
115
+ proxySession = current;
116
+ if (proxySession.providerCheckoutSessionId !== null) {
117
+ assertExistingBindingMatches(proxySession, psp);
118
+ const checkoutSession = (await options.stripe.checkout.sessions.retrieve(proxySession.providerCheckoutSessionId, undefined, options.stripeRequestOptions));
119
+ assertCheckoutSessionBelongsToProxy(checkoutSession, proxySession.id);
120
+ return {
121
+ binding: bindingFromSession(proxySession),
122
+ cart,
123
+ checkoutSession,
124
+ clientSecret: checkoutSession.client_secret ?? null,
125
+ offer,
126
+ outcome: "ready",
127
+ proxySession,
128
+ proxySessionId: proxySession.id,
129
+ };
130
+ }
131
+ if (proxySession.currentAcquisitionAttempt !== null) {
132
+ // A competing acquisition now owns the reconciled cart. Do not roll it
133
+ // back underneath that owner.
134
+ rollbackCart = undefined;
135
+ throw new ProxyCheckoutValidationError(`Proxy session ${proxySession.id} already has acquisition ${proxySession.currentAcquisitionAttempt.id} in progress; openCheckout will not create an unrelated Stripe Checkout Session.`, { code: "provider_acquisition_in_progress", field: "current_acquisition_attempt" });
136
+ }
81
137
  const params = await options.buildCheckoutSessionParams({
82
138
  cart,
83
139
  offer,
84
140
  proxySession,
85
141
  stripe: options.stripe,
86
142
  });
143
+ assertCheckoutAutomaticRecoveryDisabled(params, proxySession.id);
144
+ const commercialMode = checkoutCommercialMode(params, proxySession.id);
87
145
  const checkoutSession = (await options.stripe.checkout.sessions.create(injectRequiredCheckoutSessionMetadata(params, proxySession.id), buildStripeRequestOptions(options, proxySession.id, psp)));
88
146
  // Fail closed if the merchant forgot to place proxy_session_id metadata.
89
147
  assertCheckoutSessionBelongsToProxy(checkoutSession, proxySession.id);
90
148
  const binding = await recordOrReconcileProviderBinding({
91
149
  checkoutSessionId: checkoutSession.id,
150
+ expectedCartVersion: proxySession.cartVersion,
151
+ commercialMode,
92
152
  proxy: options.proxy,
93
153
  proxySessionId: proxySession.id,
94
154
  psp,
@@ -102,6 +162,7 @@ export async function openCheckout(options) {
102
162
  checkoutSession,
103
163
  clientSecret: checkoutSession.client_secret ?? null,
104
164
  offer,
165
+ outcome: "ready",
105
166
  proxySession,
106
167
  proxySessionId: proxySession.id,
107
168
  };
@@ -119,6 +180,39 @@ export async function openCheckout(options) {
119
180
  throw cause;
120
181
  }
121
182
  }
183
+ function terminalResultFromSession(proxySession, cart) {
184
+ const common = {
185
+ cart,
186
+ proxySession,
187
+ proxySessionId: proxySession.id,
188
+ };
189
+ switch (proxySession.presentationState) {
190
+ case "already_paid":
191
+ if (proxySession.status !== "paid" &&
192
+ proxySession.status !== "provisionable" &&
193
+ proxySession.status !== "provisioned" &&
194
+ proxySession.status !== "provisioning_failed") {
195
+ break;
196
+ }
197
+ return { ...common, outcome: "already_paid", sessionStatus: proxySession.status };
198
+ case "action_required":
199
+ if (proxySession.status !== "merchant_action_required") {
200
+ break;
201
+ }
202
+ return { ...common, outcome: "action_required", sessionStatus: proxySession.status };
203
+ case "unavailable":
204
+ if (proxySession.status !== "cancelled" &&
205
+ proxySession.status !== "created" &&
206
+ proxySession.status !== "expired" &&
207
+ proxySession.status !== "failed") {
208
+ break;
209
+ }
210
+ return { ...common, outcome: "unavailable", sessionStatus: proxySession.status };
211
+ case "checkout_available":
212
+ break;
213
+ }
214
+ throw new ProxyCheckoutValidationError(`Proxy session ${proxySession.id} returned an inconsistent presentation and lifecycle status.`, { code: "invalid_session_presentation", field: "presentation_state" });
215
+ }
122
216
  function buildStripeRequestOptions(options, proxySessionId, psp) {
123
217
  return {
124
218
  ...options.stripeRequestOptions,
@@ -167,6 +261,8 @@ async function rollbackReconciledCart(input) {
167
261
  async function recordOrReconcileProviderBinding(input) {
168
262
  try {
169
263
  return await input.proxy.sessions.recordProviderBinding(input.proxySessionId, {
264
+ commercialMode: input.commercialMode,
265
+ expectedCartVersion: input.expectedCartVersion,
170
266
  providerCheckoutSessionId: input.checkoutSessionId,
171
267
  psp: input.psp,
172
268
  requestId: input.requestId,
@@ -187,6 +283,37 @@ async function recordOrReconcileProviderBinding(input) {
187
283
  });
188
284
  }
189
285
  }
286
+ function sameCheckoutCart(actual, expected) {
287
+ return (actual.amountMinor === expected.amountMinor &&
288
+ actual.currency === expected.currency &&
289
+ canonicalJson(actual.cartSnapshot) === canonicalJson(expected.cartSnapshot));
290
+ }
291
+ function canonicalJson(value) {
292
+ if (Array.isArray(value)) {
293
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
294
+ }
295
+ if (typeof value === "object" && value !== null) {
296
+ return `{${Object.entries(value)
297
+ .sort(([left], [right]) => left.localeCompare(right))
298
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
299
+ .join(",")}}`;
300
+ }
301
+ return JSON.stringify(value);
302
+ }
303
+ function checkoutCommercialMode(params, proxySessionId) {
304
+ if (params.mode === "payment") {
305
+ return "one_time";
306
+ }
307
+ if (params.mode === "subscription") {
308
+ return "subscription";
309
+ }
310
+ throw new ProxyCheckoutValidationError(`Proxy session ${proxySessionId} requires Stripe Checkout mode payment or subscription.`, { code: "unsupported_provider_option", field: "mode" });
311
+ }
312
+ function assertCheckoutAutomaticRecoveryDisabled(params, proxySessionId) {
313
+ if (params.after_expiration?.recovery?.enabled === true) {
314
+ 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" });
315
+ }
316
+ }
190
317
  async function readReconciledBinding(input) {
191
318
  const current = await input.proxy.sessions.retrieve(input.proxySessionId, {
192
319
  requestId: input.requestId,
@@ -27,6 +27,11 @@ export interface StripeCheckoutSessionUpdateParams {
27
27
  readonly metadata?: StripeMetadataParam;
28
28
  }
29
29
  export type StripeCheckoutSessionCreateParams = object & {
30
+ readonly after_expiration?: {
31
+ readonly recovery?: {
32
+ readonly enabled?: boolean;
33
+ };
34
+ };
30
35
  readonly metadata?: StripeMetadataParam;
31
36
  readonly mode?: "payment" | "setup" | "subscription";
32
37
  readonly payment_intent_data?: unknown;
@@ -1,2 +1,2 @@
1
1
  export declare const proxyCheckoutStripeServerSdkName = "@proxy-checkout/stripe-server-js";
2
- export declare const proxyCheckoutStripeServerSdkVersion = "0.0.1";
2
+ export declare const proxyCheckoutStripeServerSdkVersion = "0.1.0-prx-124.139.1";
@@ -1,2 +1,2 @@
1
1
  export const proxyCheckoutStripeServerSdkName = "@proxy-checkout/stripe-server-js";
2
- export const proxyCheckoutStripeServerSdkVersion = "0.0.1";
2
+ export const proxyCheckoutStripeServerSdkVersion = "0.1.0-prx-124.139.1";
package/package.json CHANGED
@@ -7,14 +7,14 @@
7
7
  "sideEffects": false,
8
8
  "type": "module",
9
9
  "types": "./dist/esm/index.d.ts",
10
- "version": "0.0.1",
10
+ "version": "0.1.0-prx-124.139.1",
11
11
  "devDependencies": {
12
12
  "@types/node": "^24.12.4",
13
13
  "@vitest/coverage-v8": "4.1.7",
14
14
  "stripe": "^21.0.1",
15
15
  "typescript": "^6.0.3",
16
16
  "vitest": "4.1.7",
17
- "@proxy-checkout/server-js": "0.0.4"
17
+ "@proxy-checkout/server-js": "0.1.0-prx-124.139.1"
18
18
  },
19
19
  "exports": {
20
20
  ".": {
@@ -41,7 +41,7 @@
41
41
  "sdk"
42
42
  ],
43
43
  "peerDependencies": {
44
- "@proxy-checkout/server-js": ">=0.0.4-0 <0.1.0"
44
+ "@proxy-checkout/server-js": ">=0.1.0-0 <0.2.0"
45
45
  },
46
46
  "publishConfig": {
47
47
  "access": "public"
@@ -54,6 +54,7 @@
54
54
  "scripts": {
55
55
  "build": "node ../../scripts/sdk/build-dual-package.mjs",
56
56
  "test:all": "vitest run",
57
+ "test:ci": "pnpm run test:coverage",
57
58
  "test:coverage": "vitest run --coverage.enabled --coverage.reportsDirectory=coverage/all",
58
59
  "test:coverage:changed": "vitest run --coverage.enabled --coverage.reportsDirectory=coverage/changed",
59
60
  "test:coverage:integration": "vitest run --project integration --coverage.enabled --coverage.reportsDirectory=coverage/integration",