@proxy-checkout/stripe-server-js 0.0.0-pr-76.18.1 → 0.0.0-pr-76.20.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 the typed cart, supplies required metadata, records the Proxy provider binding, and returns the client secret. The merchant only supplies an app-specific Stripe-session builder.
7
+ - **`openCheckout(...)`** — records payer-opened, validates the typed cart, 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.
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.
@@ -32,13 +32,11 @@ const checkout = await openCheckout({
32
32
  proxySessionId,
33
33
  cartSchema, // a zod-style schema, or any (value) => Cart validator
34
34
  validateCart: ({ cart }) => buildMerchantOffer(cart),
35
- createCheckoutSession: ({ stripe, cart, metadata }) =>
36
- stripe.checkout.sessions.create({
37
- ...buildMerchantStripeParams(cart),
38
- ui_mode: "embedded",
39
- metadata: { ...metadata.checkoutSession },
40
- subscription_data: { metadata: metadata.subscriptionData.metadata },
41
- }),
35
+ buildCheckoutSessionParams: ({ cart, offer }) => ({
36
+ ...buildMerchantStripeParams(cart, offer),
37
+ mode: "subscription",
38
+ ui_mode: "embedded",
39
+ }),
42
40
  });
43
41
  // checkout.clientSecret -> render Stripe Elements
44
42
 
@@ -55,4 +53,6 @@ await syncCheckoutCart({
55
53
  });
56
54
  ```
57
55
 
58
- See the [agentic delegated-checkout guide](https://github.com/linuslabs/proxy/blob/main/docs/integration/agentic-delegated-checkout-guide.md) for the full four-route integration recipe.
56
+ Use the delegated-checkout guide from your Proxy onboarding materials for the
57
+ full four-route integration recipe. Do not link customers or coding agents to
58
+ the private Proxy repository from published package docs.
package/dist/errors.d.ts CHANGED
@@ -4,6 +4,24 @@
4
4
  * restored to its prior state, so callers can react deterministically.
5
5
  */
6
6
  export type ProxyStripeCartSyncCode = "reconciliation_failed" | "stripe_update_failed";
7
+ export type ProxyStripeOpenCheckoutCode = "provider_binding_failed";
8
+ /**
9
+ * Raised after Stripe Checkout creation succeeds but the Proxy binding cannot
10
+ * be recorded or reconciled. The provider id is exposed so operators can find
11
+ * and expire/refund the orphaned provider object deterministically.
12
+ */
13
+ export declare class ProxyStripeOpenCheckoutError extends Error {
14
+ readonly name = "ProxyStripeOpenCheckoutError";
15
+ readonly code: ProxyStripeOpenCheckoutCode;
16
+ readonly cause: unknown;
17
+ readonly providerCheckoutSessionId: string;
18
+ readonly reconciled = false;
19
+ constructor(message: string, details: {
20
+ cause: unknown;
21
+ code: ProxyStripeOpenCheckoutCode;
22
+ providerCheckoutSessionId: string;
23
+ });
24
+ }
7
25
  export declare class ProxyStripeCartSyncError extends Error {
8
26
  readonly name = "ProxyStripeCartSyncError";
9
27
  readonly code: ProxyStripeCartSyncCode;
package/dist/errors.js CHANGED
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Raised after Stripe Checkout creation succeeds but the Proxy binding cannot
3
+ * be recorded or reconciled. The provider id is exposed so operators can find
4
+ * and expire/refund the orphaned provider object deterministically.
5
+ */
6
+ export class ProxyStripeOpenCheckoutError extends Error {
7
+ name = "ProxyStripeOpenCheckoutError";
8
+ code;
9
+ cause;
10
+ providerCheckoutSessionId;
11
+ reconciled = false;
12
+ constructor(message, details) {
13
+ super(message);
14
+ this.cause = details.cause;
15
+ this.code = details.code;
16
+ this.providerCheckoutSessionId = details.providerCheckoutSessionId;
17
+ }
18
+ }
1
19
  export class ProxyStripeCartSyncError extends Error {
2
20
  name = "ProxyStripeCartSyncError";
3
21
  code;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { type ProxyStripeCartSyncCode, ProxyStripeCartSyncError, } from "./errors.js";
2
- export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, PROXY_SESSION_METADATA_KEY, type ProxySessionMetadata, type ProxyStripeRequiredMetadata, requiredMetadata, } from "./metadata.js";
3
- export { type OpenCheckoutOptions, type OpenCheckoutResult, openCheckout, } from "./open-checkout.js";
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";
4
4
  export { normalizePsp } from "./psp.js";
5
5
  export { type SyncCheckoutCartOptions, type SyncCheckoutCartResult, type SyncedCart, syncCheckoutCart, } from "./sync-checkout-cart.js";
6
- export type { StripeCheckoutSessionLike, StripeCheckoutSessionsApiLike, StripeCheckoutSessionUpdateParams, StripeClientLike, StripeMetadata, StripePaymentIntentLike, StripeSubscriptionLike, } from "./types.js";
6
+ export type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripeCheckoutSessionsApiLike, StripeCheckoutSessionUpdateParams, StripeClientLike, StripeMetadata, StripePaymentIntentLike, StripeRequestOptionsLike, StripeSubscriptionLike, } from "./types.js";
7
7
  export { proxyCheckoutStripeServerSdkName, proxyCheckoutStripeServerSdkVersion, } from "./version.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { ProxyStripeCartSyncError, } from "./errors.js";
2
- export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, PROXY_SESSION_METADATA_KEY, requiredMetadata, } from "./metadata.js";
1
+ export { ProxyStripeCartSyncError, ProxyStripeOpenCheckoutError, } from "./errors.js";
2
+ export { assertCheckoutSessionBelongsToProxy, assertPaymentIntentBelongsToProxy, assertSubscriptionBelongsToProxy, injectRequiredCheckoutSessionMetadata, 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";
@@ -6,7 +6,7 @@
6
6
  * customers never have to remember which Stripe objects need the key, and
7
7
  * provides assertions that a Stripe object actually belongs to a Proxy session.
8
8
  */
9
- import type { StripeCheckoutSessionLike, StripePaymentIntentLike, StripeSubscriptionLike } from "./types.js";
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
12
  export type ProxySessionMetadata = Readonly<Record<"proxy_session_id", string>>;
@@ -32,6 +32,12 @@ export interface ProxyStripeRequiredMetadata {
32
32
  export declare function requiredMetadata(proxySession: {
33
33
  id: string;
34
34
  }): ProxyStripeRequiredMetadata;
35
+ /**
36
+ * Return Stripe Checkout Session create params with required Proxy metadata in
37
+ * every object Proxy may later need to correlate. Conflicting merchant-provided
38
+ * proxy metadata fails before any Stripe side effect.
39
+ */
40
+ export declare function injectRequiredCheckoutSessionMetadata(params: StripeCheckoutSessionCreateParams, proxySessionId: string): StripeCheckoutSessionCreateParams;
35
41
  /** Assert a Stripe Checkout Session carries the expected `proxy_session_id` metadata. */
36
42
  export declare function assertCheckoutSessionBelongsToProxy(checkoutSession: Pick<StripeCheckoutSessionLike, "metadata">, proxySessionId: string): void;
37
43
  /** Assert a Stripe Subscription carries the expected `proxy_session_id` metadata. */
package/dist/metadata.js CHANGED
@@ -20,6 +20,34 @@ export function requiredMetadata(proxySession) {
20
20
  subscriptionData: { metadata },
21
21
  };
22
22
  }
23
+ /**
24
+ * Return Stripe Checkout Session create params with required Proxy metadata in
25
+ * every object Proxy may later need to correlate. Conflicting merchant-provided
26
+ * proxy metadata fails before any Stripe side effect.
27
+ */
28
+ export function injectRequiredCheckoutSessionMetadata(params, proxySessionId) {
29
+ const metadata = { proxy_session_id: proxySessionId };
30
+ return {
31
+ ...params,
32
+ metadata: mergeProxyMetadata(params.metadata, metadata, "metadata.proxy_session_id"),
33
+ ...(params.mode === "subscription" || params.subscription_data !== undefined
34
+ ? {
35
+ subscription_data: {
36
+ ...params.subscription_data,
37
+ metadata: mergeProxyMetadata(params.subscription_data?.metadata, metadata, "subscription_data.metadata.proxy_session_id"),
38
+ },
39
+ }
40
+ : {}),
41
+ ...(params.mode === "payment" || params.payment_intent_data !== undefined
42
+ ? {
43
+ payment_intent_data: {
44
+ ...params.payment_intent_data,
45
+ metadata: mergeProxyMetadata(params.payment_intent_data?.metadata, metadata, "payment_intent_data.metadata.proxy_session_id"),
46
+ },
47
+ }
48
+ : {}),
49
+ };
50
+ }
23
51
  /** Assert a Stripe Checkout Session carries the expected `proxy_session_id` metadata. */
24
52
  export function assertCheckoutSessionBelongsToProxy(checkoutSession, proxySessionId) {
25
53
  assertProxyMetadata(checkoutSession.metadata, proxySessionId, "Stripe checkout session");
@@ -38,3 +66,13 @@ function assertProxyMetadata(metadata, proxySessionId, objectLabel) {
38
66
  throw new ProxyCheckoutValidationError(`${objectLabel} metadata.${PROXY_SESSION_METADATA_KEY} (${value ?? "missing"}) does not match Proxy session ${proxySessionId}.`, { code: "provider_binding_mismatch", field: `metadata.${PROXY_SESSION_METADATA_KEY}` });
39
67
  }
40
68
  }
69
+ 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 });
73
+ }
74
+ return {
75
+ ...existing,
76
+ ...required,
77
+ };
78
+ }
@@ -2,24 +2,22 @@
2
2
  * `openCheckout` — the payer-opened + provider-object creation workflow.
3
3
  *
4
4
  * It performs the Proxy protocol steps (record payer-opened, validate the cart,
5
- * supply required metadata, record the provider binding) and delegates only the
6
- * app-specific Stripe object creation to the merchant callback. This makes it
7
- * hard to create a Stripe checkout that is not correctly linked to a session.
5
+ * inject required metadata, create the Stripe Checkout Session, record the
6
+ * provider binding). Merchants provide only Stripe params, so the adapter owns
7
+ * metadata placement, idempotency, and binding reconciliation.
8
8
  */
9
- import type { PayerOpenedResult, ProxyCartValidator, ProxyCheckoutServerClient } from "@proxy-checkout/server-js";
10
- import type { ProxyStripeRequiredMetadata } from "./metadata.js";
11
- import type { StripeCheckoutSessionLike } from "./types.js";
12
- export interface OpenCheckoutOptions<TStripe, TCart, TOffer, TSession> {
13
- /** Optional schema validating the session cart snapshot into a typed cart. */
14
- readonly cartSchema?: ProxyCartValidator<TCart>;
15
- /** Builds the Stripe checkout object. Must place `metadata.checkoutSession` (and subscription/payment-intent metadata) on the created object. */
16
- readonly createCheckoutSession: (input: {
9
+ import type { JsonObject, PayerOpenedResult, ProxyCartValidator, ProxyCheckoutServerClient, ProxySessionProviderBinding } from "@proxy-checkout/server-js";
10
+ import type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripeClientLike, StripeRequestOptionsWithoutIdempotencyLike } from "./types.js";
11
+ export interface OpenCheckoutOptions<TStripe extends StripeClientLike, TCart, TOffer> {
12
+ /** Builds app-specific Stripe Checkout Session params. Proxy metadata is injected by the SDK. */
13
+ readonly buildCheckoutSessionParams: (input: {
17
14
  cart: TCart;
18
- metadata: ProxyStripeRequiredMetadata;
19
15
  offer: TOffer;
20
16
  proxySession: PayerOpenedResult;
21
17
  stripe: TStripe;
22
- }) => Promise<TSession> | TSession;
18
+ }) => Promise<StripeCheckoutSessionCreateParams> | StripeCheckoutSessionCreateParams;
19
+ /** Optional schema validating the session cart snapshot into a typed cart. */
20
+ readonly cartSchema?: ProxyCartValidator<TCart>;
23
21
  /** Optional extractor used to assert the cart buyer reference matches the session. */
24
22
  readonly getCartBuyerReference?: (cart: TCart) => string | null | undefined;
25
23
  /** A Proxy server SDK client. */
@@ -27,17 +25,31 @@ export interface OpenCheckoutOptions<TStripe, TCart, TOffer, TSession> {
27
25
  readonly proxySessionId: string;
28
26
  /** Provider identifier recorded with the binding. Defaults to "stripe". */
29
27
  readonly psp?: string;
28
+ /** Optional cart reconciliation before Stripe creation. The SDK performs the versioned Proxy cart write. */
29
+ readonly reconcileCart?: (input: {
30
+ cart: TCart;
31
+ offer: TOffer;
32
+ session: PayerOpenedResult;
33
+ }) => OpenCheckoutReconciledCart<TCart> | Promise<OpenCheckoutReconciledCart<TCart>> | null | undefined;
30
34
  readonly requestId?: string;
31
- /** The merchant's Stripe instance, forwarded to `createCheckoutSession`. */
35
+ /** The merchant's Stripe instance. */
32
36
  readonly stripe: TStripe;
37
+ /** Optional Stripe request options. The SDK owns the deterministic idempotency key. */
38
+ readonly stripeRequestOptions?: StripeRequestOptionsWithoutIdempotencyLike;
33
39
  /** Optional merchant cart validation that derives an app-specific offer. */
34
40
  readonly validateCart?: (input: {
35
41
  cart: TCart;
36
42
  session: PayerOpenedResult;
37
43
  }) => Promise<TOffer> | TOffer;
38
44
  }
45
+ export interface OpenCheckoutReconciledCart<TCart> {
46
+ readonly amountMinor: number;
47
+ readonly cart: TCart;
48
+ readonly cartSnapshot: JsonObject;
49
+ readonly currency: string;
50
+ }
39
51
  export interface OpenCheckoutResult<TCart, TOffer, TSession> {
40
- readonly binding: Awaited<ReturnType<ProxyCheckoutServerClient["sessions"]["recordProviderBinding"]>>;
52
+ readonly binding: ProxySessionProviderBinding;
41
53
  readonly cart: TCart;
42
54
  readonly checkoutSession: TSession;
43
55
  readonly clientSecret: string | null;
@@ -45,4 +57,4 @@ export interface OpenCheckoutResult<TCart, TOffer, TSession> {
45
57
  readonly proxySession: PayerOpenedResult;
46
58
  readonly proxySessionId: string;
47
59
  }
48
- export declare function openCheckout<TStripe, TCart = unknown, TOffer = undefined, TSession extends StripeCheckoutSessionLike = StripeCheckoutSessionLike>(options: OpenCheckoutOptions<TStripe, TCart, TOffer, TSession>): Promise<OpenCheckoutResult<TCart, TOffer, TSession>>;
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>>;
@@ -2,43 +2,80 @@
2
2
  * `openCheckout` — the payer-opened + provider-object creation workflow.
3
3
  *
4
4
  * It performs the Proxy protocol steps (record payer-opened, validate the cart,
5
- * supply required metadata, record the provider binding) and delegates only the
6
- * app-specific Stripe object creation to the merchant callback. This makes it
7
- * hard to create a Stripe checkout that is not correctly linked to a session.
5
+ * inject required metadata, create the Stripe Checkout Session, record the
6
+ * provider binding). Merchants provide only Stripe params, so the adapter owns
7
+ * metadata placement, idempotency, and binding reconciliation.
8
8
  */
9
- import { assertCheckoutSessionBelongsToProxy, requiredMetadata } from "./metadata.js";
9
+ import { ProxyCheckoutValidationError } from "@proxy-checkout/server-js";
10
+ import { ProxyStripeOpenCheckoutError } from "./errors.js";
11
+ import { assertCheckoutSessionBelongsToProxy, injectRequiredCheckoutSessionMetadata, } from "./metadata.js";
10
12
  import { normalizePsp } from "./psp.js";
11
13
  export async function openCheckout(options) {
12
14
  // Normalize the same way the API does on store, so the binding is recorded
13
15
  // under the normalized psp and later syncCheckoutCart validations compare
14
16
  // normalized-to-normalized.
15
17
  const psp = normalizePsp(options.psp ?? "stripe");
16
- const proxySession = await options.proxy.sessions.payerOpened(options.proxySessionId, {
18
+ let proxySession = await options.proxy.sessions.payerOpened(options.proxySessionId, {
17
19
  requestId: options.requestId,
18
20
  });
19
21
  // Parse the cart (and run the buyer-reference check) whenever a schema OR a
20
22
  // buyer-reference extractor is provided, so the consistency check is never
21
23
  // silently skipped just because no custom schema was passed.
22
- const cart = options.cartSchema || options.getCartBuyerReference
24
+ let cart = options.cartSchema || options.getCartBuyerReference
23
25
  ? options.proxy.sessions.parseCart(proxySession, options.cartSchema ?? ((value) => value), { getBuyerReference: options.getCartBuyerReference })
24
26
  : proxySession.cartSnapshot;
25
27
  const offer = options.validateCart
26
28
  ? await options.validateCart({ cart, session: proxySession })
27
29
  : undefined;
28
- const metadata = requiredMetadata(proxySession);
29
- const checkoutSession = await options.createCheckoutSession({
30
+ const reconciledCart = await options.reconcileCart?.({ cart, offer, session: proxySession });
31
+ if (reconciledCart) {
32
+ const updatedCart = await options.proxy.sessions.cart.set(proxySession.id, {
33
+ amountMinor: reconciledCart.amountMinor,
34
+ cartSnapshot: reconciledCart.cartSnapshot,
35
+ currency: reconciledCart.currency,
36
+ expectedCartVersion: proxySession.cartVersion,
37
+ requestId: options.requestId,
38
+ });
39
+ proxySession = {
40
+ ...proxySession,
41
+ amountMinor: updatedCart.amountMinor,
42
+ cartSnapshot: updatedCart.cartSnapshot,
43
+ cartVersion: updatedCart.cartVersion,
44
+ currency: updatedCart.currency,
45
+ updatedAt: proxySession.updatedAt,
46
+ };
47
+ cart = reconciledCart.cart;
48
+ }
49
+ if (proxySession.providerCheckoutSessionId !== null) {
50
+ assertExistingBindingMatches(proxySession, psp);
51
+ const checkoutSession = (await options.stripe.checkout.sessions.retrieve(proxySession.providerCheckoutSessionId));
52
+ assertCheckoutSessionBelongsToProxy(checkoutSession, proxySession.id);
53
+ return {
54
+ binding: bindingFromSession(proxySession),
55
+ cart,
56
+ checkoutSession,
57
+ clientSecret: checkoutSession.client_secret ?? null,
58
+ offer,
59
+ proxySession,
60
+ proxySessionId: proxySession.id,
61
+ };
62
+ }
63
+ const params = await options.buildCheckoutSessionParams({
30
64
  cart,
31
- metadata,
32
65
  offer,
33
66
  proxySession,
34
67
  stripe: options.stripe,
35
68
  });
69
+ const checkoutSession = (await options.stripe.checkout.sessions.create(injectRequiredCheckoutSessionMetadata(params, proxySession.id), buildStripeRequestOptions(options, proxySession.id, psp)));
36
70
  // Fail closed if the merchant forgot to place proxy_session_id metadata.
37
71
  assertCheckoutSessionBelongsToProxy(checkoutSession, proxySession.id);
38
- const binding = await options.proxy.sessions.recordProviderBinding(proxySession.id, {
39
- providerCheckoutSessionId: checkoutSession.id,
72
+ const binding = await recordOrReconcileProviderBinding({
73
+ checkoutSessionId: checkoutSession.id,
74
+ proxy: options.proxy,
75
+ proxySessionId: proxySession.id,
40
76
  psp,
41
77
  requestId: options.requestId,
78
+ stripe: options.stripe,
42
79
  });
43
80
  return {
44
81
  binding,
@@ -50,3 +87,70 @@ export async function openCheckout(options) {
50
87
  proxySessionId: proxySession.id,
51
88
  };
52
89
  }
90
+ function buildStripeRequestOptions(options, proxySessionId, psp) {
91
+ return {
92
+ ...options.stripeRequestOptions,
93
+ idempotencyKey: `proxy-open-checkout:${proxySessionId}:${psp}`,
94
+ };
95
+ }
96
+ function assertExistingBindingMatches(proxySession, psp) {
97
+ if (proxySession.providerCheckoutSessionId === null) {
98
+ return;
99
+ }
100
+ if (proxySession.providerCheckoutSessionPsp !== psp) {
101
+ throw new ProxyCheckoutValidationError(`Proxy session ${proxySession.id} is already bound under psp ${proxySession.providerCheckoutSessionPsp}.`, { code: "provider_binding_mismatch", field: "provider_checkout_session_psp" });
102
+ }
103
+ }
104
+ function bindingFromSession(proxySession) {
105
+ if (proxySession.providerCheckoutSessionId === null ||
106
+ proxySession.providerCheckoutSessionPsp === null) {
107
+ throw new Error("Proxy session is not bound to a provider checkout session.");
108
+ }
109
+ return {
110
+ providerCheckoutSessionId: proxySession.providerCheckoutSessionId,
111
+ proxySessionId: proxySession.id,
112
+ psp: proxySession.providerCheckoutSessionPsp,
113
+ updatedAt: proxySession.updatedAt,
114
+ };
115
+ }
116
+ async function recordOrReconcileProviderBinding(input) {
117
+ try {
118
+ return await input.proxy.sessions.recordProviderBinding(input.proxySessionId, {
119
+ providerCheckoutSessionId: input.checkoutSessionId,
120
+ psp: input.psp,
121
+ requestId: input.requestId,
122
+ });
123
+ }
124
+ catch (cause) {
125
+ const reconciled = await readReconciledBinding(input).catch(() => undefined);
126
+ if (reconciled) {
127
+ return reconciled;
128
+ }
129
+ if (isProxyConflict(cause)) {
130
+ throw cause;
131
+ }
132
+ throw new ProxyStripeOpenCheckoutError("Stripe Checkout Session was created but Proxy provider binding could not be recorded.", {
133
+ cause,
134
+ code: "provider_binding_failed",
135
+ providerCheckoutSessionId: input.checkoutSessionId,
136
+ });
137
+ }
138
+ }
139
+ async function readReconciledBinding(input) {
140
+ const current = await input.proxy.sessions.retrieve(input.proxySessionId, {
141
+ requestId: input.requestId,
142
+ });
143
+ if (current.providerCheckoutSessionId !== input.checkoutSessionId ||
144
+ current.providerCheckoutSessionPsp !== input.psp) {
145
+ return undefined;
146
+ }
147
+ const checkoutSession = await input.stripe.checkout.sessions.retrieve(input.checkoutSessionId);
148
+ assertCheckoutSessionBelongsToProxy(checkoutSession, input.proxySessionId);
149
+ return bindingFromSession(current);
150
+ }
151
+ function isProxyConflict(error) {
152
+ return (typeof error === "object" &&
153
+ error !== null &&
154
+ "statusCode" in error &&
155
+ error.statusCode === 409);
156
+ }
package/dist/psp.d.ts CHANGED
@@ -8,5 +8,5 @@
8
8
  * casing/whitespace for `openCheckout` vs `syncCheckoutCart` does not get a
9
9
  * false `provider_binding_mismatch`.
10
10
  */
11
- /** Normalize a psp identifier exactly as the Proxy API does before storing/comparing a binding. */
11
+ /** Normalize and validate a psp identifier exactly as the Proxy API does before storing/comparing a binding. */
12
12
  export declare function normalizePsp(psp: string): string;
package/dist/psp.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ProxyCheckoutValidationError } from "@proxy-checkout/server-js";
1
2
  /**
2
3
  * Provider (psp) identifier normalization, kept in lockstep with the Proxy API.
3
4
  *
@@ -8,7 +9,14 @@
8
9
  * casing/whitespace for `openCheckout` vs `syncCheckoutCart` does not get a
9
10
  * false `provider_binding_mismatch`.
10
11
  */
11
- /** Normalize a psp identifier exactly as the Proxy API does before storing/comparing a binding. */
12
+ /** Normalize and validate a psp identifier exactly as the Proxy API does before storing/comparing a binding. */
12
13
  export function normalizePsp(psp) {
13
- return psp.trim().toLowerCase();
14
+ const normalized = psp.trim().toLowerCase();
15
+ if (!/^[a-z0-9_]{1,32}$/.test(normalized)) {
16
+ throw new ProxyCheckoutValidationError("psp must be a short lowercase identifier", {
17
+ code: "provider_binding_mismatch",
18
+ field: "psp",
19
+ });
20
+ }
21
+ return normalized;
14
22
  }
@@ -33,13 +33,15 @@ export async function syncCheckoutCart(options) {
33
33
  const prior = {
34
34
  amountMinor: session.amountMinor,
35
35
  cartSnapshot: session.cartSnapshot,
36
+ cartVersion: session.cartVersion,
36
37
  currency: session.currency,
37
38
  };
38
39
  // Update Proxy first; the API rejects a non-editable session before Stripe is touched.
39
- await options.proxy.sessions.cart.set(options.proxySessionId, {
40
+ const proxyCartUpdate = await options.proxy.sessions.cart.set(options.proxySessionId, {
40
41
  amountMinor: next.amountMinor,
41
42
  cartSnapshot: next.cartSnapshot,
42
43
  currency: next.currency,
44
+ expectedCartVersion: session.cartVersion,
43
45
  requestId: options.requestId,
44
46
  });
45
47
  let updatedCheckout;
@@ -54,6 +56,7 @@ export async function syncCheckoutCart(options) {
54
56
  amountMinor: prior.amountMinor,
55
57
  cartSnapshot: prior.cartSnapshot,
56
58
  currency: prior.currency,
59
+ expectedCartVersion: proxyCartUpdate.cartVersion,
57
60
  requestId: options.requestId,
58
61
  });
59
62
  }
package/dist/types.d.ts CHANGED
@@ -24,7 +24,29 @@ export interface StripeCheckoutSessionUpdateParams {
24
24
  readonly line_items?: unknown;
25
25
  readonly metadata?: Record<string, string>;
26
26
  }
27
+ export interface StripeCheckoutSessionCreateParams {
28
+ readonly metadata?: Record<string, string>;
29
+ readonly mode?: string;
30
+ readonly payment_intent_data?: {
31
+ readonly metadata?: Record<string, string>;
32
+ readonly [key: string]: unknown;
33
+ };
34
+ readonly subscription_data?: {
35
+ readonly metadata?: Record<string, string>;
36
+ readonly [key: string]: unknown;
37
+ };
38
+ readonly [key: string]: unknown;
39
+ }
40
+ export interface StripeRequestOptionsLike {
41
+ readonly idempotencyKey?: string;
42
+ readonly [key: string]: unknown;
43
+ }
44
+ export interface StripeRequestOptionsWithoutIdempotencyLike {
45
+ readonly idempotencyKey?: never;
46
+ readonly [key: string]: unknown;
47
+ }
27
48
  export interface StripeCheckoutSessionsApiLike {
49
+ create(params: StripeCheckoutSessionCreateParams, options?: StripeRequestOptionsLike): Promise<StripeCheckoutSessionLike>;
28
50
  retrieve(id: string, params?: unknown, options?: unknown): Promise<StripeCheckoutSessionLike>;
29
51
  update(id: string, params: StripeCheckoutSessionUpdateParams, options?: unknown): Promise<StripeCheckoutSessionLike>;
30
52
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export declare const proxyCheckoutStripeServerSdkName = "@proxy-checkout/stripe-server-js";
2
- export declare const proxyCheckoutStripeServerSdkVersion = "0.0.0-pr-76.18.1";
2
+ export declare const proxyCheckoutStripeServerSdkVersion = "0.0.0-pr-76.20.1";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const proxyCheckoutStripeServerSdkName = "@proxy-checkout/stripe-server-js";
2
- export const proxyCheckoutStripeServerSdkVersion = "0.0.0-pr-76.18.1";
2
+ export const proxyCheckoutStripeServerSdkVersion = "0.0.0-pr-76.20.1";
package/package.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "sideEffects": false,
8
8
  "type": "module",
9
9
  "types": "./dist/index.d.ts",
10
- "version": "0.0.0-pr-76.18.1",
10
+ "version": "0.0.0-pr-76.20.1",
11
11
  "devDependencies": {
12
12
  "@types/node": "^24.12.4",
13
13
  "@vitest/coverage-v8": "4.1.7",
14
14
  "typescript": "^6.0.3",
15
15
  "vitest": "4.1.7",
16
- "@proxy-checkout/server-js": "0.0.4-pr-76.18.1"
16
+ "@proxy-checkout/server-js": "0.0.4-pr-76.20.1"
17
17
  },
18
18
  "exports": {
19
19
  ".": {