@proxy-checkout/stripe-server-js 0.0.0 → 0.0.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 +8 -9
- package/dist/cjs/errors.d.cts +38 -0
- package/dist/cjs/errors.d.ts +38 -0
- package/dist/cjs/errors.js +38 -0
- package/dist/cjs/index.d.cts +7 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/metadata.d.cts +46 -0
- package/dist/{metadata.d.ts → cjs/metadata.d.ts} +7 -1
- package/dist/cjs/metadata.js +92 -0
- package/dist/cjs/open-checkout.d.cts +60 -0
- package/dist/cjs/open-checkout.d.ts +60 -0
- package/dist/cjs/open-checkout.js +210 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/psp.d.cts +12 -0
- package/dist/cjs/psp.d.ts +12 -0
- package/dist/cjs/psp.js +25 -0
- package/dist/cjs/sync-checkout-cart.d.cts +47 -0
- package/dist/{sync-checkout-cart.d.ts → cjs/sync-checkout-cart.d.ts} +3 -1
- package/dist/cjs/sync-checkout-cart.js +97 -0
- package/dist/cjs/types.d.cts +52 -0
- package/dist/{types.d.ts → cjs/types.d.ts} +20 -3
- package/dist/cjs/types.js +10 -0
- package/dist/cjs/version.d.cts +2 -0
- package/dist/{version.d.ts → cjs/version.d.ts} +1 -1
- package/dist/cjs/version.js +5 -0
- package/dist/esm/errors.d.ts +38 -0
- package/dist/esm/errors.js +33 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/metadata.d.ts +46 -0
- package/dist/{metadata.js → esm/metadata.js} +44 -0
- package/dist/esm/open-checkout.d.ts +60 -0
- package/dist/esm/open-checkout.js +207 -0
- package/dist/esm/psp.d.ts +12 -0
- package/dist/esm/psp.js +22 -0
- package/dist/esm/sync-checkout-cart.d.ts +47 -0
- package/dist/{sync-checkout-cart.js → esm/sync-checkout-cart.js} +37 -17
- package/dist/esm/types.d.ts +52 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/{version.js → esm/version.js} +1 -1
- package/package.json +16 -8
- package/dist/errors.d.ts +0 -20
- package/dist/errors.js +0 -15
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -5
- package/dist/open-checkout.d.ts +0 -48
- package/dist/open-checkout.js +0 -48
- /package/dist/{types.js → esm/types.js} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Required Stripe metadata placement + provider-object ownership assertions.
|
|
3
|
+
*
|
|
4
|
+
* Proxy correlates Stripe objects to a session solely through
|
|
5
|
+
* `metadata.proxy_session_id`. This module centralizes that placement so
|
|
6
|
+
* customers never have to remember which Stripe objects need the key, and
|
|
7
|
+
* provides assertions that a Stripe object actually belongs to a Proxy session.
|
|
8
|
+
*/
|
|
9
|
+
import type { StripeCheckoutSessionCreateParams, StripeCheckoutSessionLike, StripePaymentIntentLike, StripeSubscriptionLike } from "./types.js";
|
|
10
|
+
/** The single metadata key Proxy reads to correlate a Stripe object to a session. */
|
|
11
|
+
export declare const PROXY_SESSION_METADATA_KEY = "proxy_session_id";
|
|
12
|
+
export type ProxySessionMetadata = Readonly<Record<"proxy_session_id", string>>;
|
|
13
|
+
/**
|
|
14
|
+
* The metadata each Stripe object must carry, ready to spread into Stripe params.
|
|
15
|
+
*
|
|
16
|
+
* - `checkoutSession` → `metadata` on `checkout.sessions.create`
|
|
17
|
+
* - `subscriptionData` → `subscription_data` for subscription-mode Checkout
|
|
18
|
+
* - `paymentIntentData` → `payment_intent_data` for payment-mode Checkout / PaymentIntents
|
|
19
|
+
*/
|
|
20
|
+
export interface ProxyStripeRequiredMetadata {
|
|
21
|
+
readonly checkoutSession: ProxySessionMetadata;
|
|
22
|
+
readonly paymentIntent: ProxySessionMetadata;
|
|
23
|
+
readonly paymentIntentData: {
|
|
24
|
+
readonly metadata: ProxySessionMetadata;
|
|
25
|
+
};
|
|
26
|
+
readonly subscription: ProxySessionMetadata;
|
|
27
|
+
readonly subscriptionData: {
|
|
28
|
+
readonly metadata: ProxySessionMetadata;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Build the required Stripe metadata placements for a Proxy session. */
|
|
32
|
+
export declare function requiredMetadata(proxySession: {
|
|
33
|
+
id: string;
|
|
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;
|
|
41
|
+
/** Assert a Stripe Checkout Session carries the expected `proxy_session_id` metadata. */
|
|
42
|
+
export declare function assertCheckoutSessionBelongsToProxy(checkoutSession: Pick<StripeCheckoutSessionLike, "metadata">, proxySessionId: string): void;
|
|
43
|
+
/** Assert a Stripe Subscription carries the expected `proxy_session_id` metadata. */
|
|
44
|
+
export declare function assertSubscriptionBelongsToProxy(subscription: Pick<StripeSubscriptionLike, "metadata">, proxySessionId: string): void;
|
|
45
|
+
/** Assert a Stripe PaymentIntent carries the expected `proxy_session_id` metadata. */
|
|
46
|
+
export declare function assertPaymentIntentBelongsToProxy(paymentIntent: Pick<StripePaymentIntentLike, "metadata">, proxySessionId: string): void;
|
|
@@ -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
|
+
...objectRecord(params.subscription_data),
|
|
37
|
+
metadata: mergeProxyMetadata(metadataFromNestedParams(params.subscription_data), 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
|
+
...objectRecord(params.payment_intent_data),
|
|
45
|
+
metadata: mergeProxyMetadata(metadataFromNestedParams(params.payment_intent_data), 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,19 @@ 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
|
+
}
|
|
79
|
+
function objectRecord(value) {
|
|
80
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
81
|
+
}
|
|
82
|
+
function metadataFromNestedParams(value) {
|
|
83
|
+
return objectRecord(value).metadata;
|
|
84
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `openCheckout` — the payer-opened + provider-object creation workflow.
|
|
3
|
+
*
|
|
4
|
+
* It performs the Proxy protocol steps (record payer-opened, validate the cart,
|
|
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
|
+
*/
|
|
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: {
|
|
14
|
+
cart: TCart;
|
|
15
|
+
offer: TOffer;
|
|
16
|
+
proxySession: PayerOpenedResult;
|
|
17
|
+
stripe: TStripe;
|
|
18
|
+
}) => Promise<StripeCheckoutSessionCreateParams> | StripeCheckoutSessionCreateParams;
|
|
19
|
+
/** Optional schema validating the session cart snapshot into a typed cart. */
|
|
20
|
+
readonly cartSchema?: ProxyCartValidator<TCart>;
|
|
21
|
+
/** Optional extractor used to assert the cart buyer reference matches the session. */
|
|
22
|
+
readonly getCartBuyerReference?: (cart: TCart) => string | null | undefined;
|
|
23
|
+
/** A Proxy server SDK client. */
|
|
24
|
+
readonly proxy: ProxyCheckoutServerClient;
|
|
25
|
+
readonly proxySessionId: string;
|
|
26
|
+
/** Provider identifier recorded with the binding. Defaults to "stripe". */
|
|
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;
|
|
34
|
+
readonly requestId?: string;
|
|
35
|
+
/** The merchant's Stripe instance. */
|
|
36
|
+
readonly stripe: TStripe;
|
|
37
|
+
/** Optional Stripe request options. The SDK owns the deterministic idempotency key. */
|
|
38
|
+
readonly stripeRequestOptions?: StripeRequestOptionsWithoutIdempotencyLike;
|
|
39
|
+
/** Optional merchant cart validation that derives an app-specific offer. */
|
|
40
|
+
readonly validateCart?: (input: {
|
|
41
|
+
cart: TCart;
|
|
42
|
+
session: PayerOpenedResult;
|
|
43
|
+
}) => Promise<TOffer> | TOffer;
|
|
44
|
+
}
|
|
45
|
+
export interface OpenCheckoutReconciledCart<TCart> {
|
|
46
|
+
readonly amountMinor: number;
|
|
47
|
+
readonly cart: TCart;
|
|
48
|
+
readonly cartSnapshot: JsonObject;
|
|
49
|
+
readonly currency: string;
|
|
50
|
+
}
|
|
51
|
+
export interface OpenCheckoutResult<TCart, TOffer, TSession> {
|
|
52
|
+
readonly binding: ProxySessionProviderBinding;
|
|
53
|
+
readonly cart: TCart;
|
|
54
|
+
readonly checkoutSession: TSession;
|
|
55
|
+
readonly clientSecret: string | null;
|
|
56
|
+
readonly offer: TOffer;
|
|
57
|
+
readonly proxySession: PayerOpenedResult;
|
|
58
|
+
readonly proxySessionId: string;
|
|
59
|
+
}
|
|
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>>;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `openCheckout` — the payer-opened + provider-object creation workflow.
|
|
3
|
+
*
|
|
4
|
+
* It performs the Proxy protocol steps (record payer-opened, validate the cart,
|
|
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
|
+
*/
|
|
9
|
+
import { assertCartBuyerReference, ProxyCheckoutValidationError } from "@proxy-checkout/server-js";
|
|
10
|
+
import { ProxyStripeCartSyncError, ProxyStripeOpenCheckoutError } from "./errors.js";
|
|
11
|
+
import { assertCheckoutSessionBelongsToProxy, injectRequiredCheckoutSessionMetadata, } from "./metadata.js";
|
|
12
|
+
import { normalizePsp } from "./psp.js";
|
|
13
|
+
export async function openCheckout(options) {
|
|
14
|
+
// Normalize the same way the API does on store, so the binding is recorded
|
|
15
|
+
// under the normalized psp and later syncCheckoutCart validations compare
|
|
16
|
+
// normalized-to-normalized.
|
|
17
|
+
const psp = normalizePsp(options.psp ?? "stripe");
|
|
18
|
+
let proxySession = await options.proxy.sessions.payerOpened(options.proxySessionId, {
|
|
19
|
+
requestId: options.requestId,
|
|
20
|
+
});
|
|
21
|
+
// Parse the cart (and run the buyer-reference check) whenever a schema OR a
|
|
22
|
+
// buyer-reference extractor is provided, so the consistency check is never
|
|
23
|
+
// silently skipped just because no custom schema was passed.
|
|
24
|
+
let cart = options.cartSchema || options.getCartBuyerReference
|
|
25
|
+
? options.proxy.sessions.parseCart(proxySession, options.cartSchema ?? ((value) => value), { getBuyerReference: options.getCartBuyerReference })
|
|
26
|
+
: proxySession.cartSnapshot;
|
|
27
|
+
let offer = options.validateCart
|
|
28
|
+
? await options.validateCart({ cart, session: proxySession })
|
|
29
|
+
: undefined;
|
|
30
|
+
if (proxySession.providerCheckoutSessionId !== null) {
|
|
31
|
+
assertExistingBindingMatches(proxySession, psp);
|
|
32
|
+
const checkoutSession = (await options.stripe.checkout.sessions.retrieve(proxySession.providerCheckoutSessionId, undefined, options.stripeRequestOptions));
|
|
33
|
+
assertCheckoutSessionBelongsToProxy(checkoutSession, proxySession.id);
|
|
34
|
+
return {
|
|
35
|
+
binding: bindingFromSession(proxySession),
|
|
36
|
+
cart,
|
|
37
|
+
checkoutSession,
|
|
38
|
+
clientSecret: checkoutSession.client_secret ?? null,
|
|
39
|
+
offer,
|
|
40
|
+
proxySession,
|
|
41
|
+
proxySessionId: proxySession.id,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
let rollbackCart;
|
|
45
|
+
const reconciledCart = await options.reconcileCart?.({ cart, offer, session: proxySession });
|
|
46
|
+
if (reconciledCart) {
|
|
47
|
+
if (options.getCartBuyerReference !== undefined) {
|
|
48
|
+
assertCartBuyerReference(options.getCartBuyerReference(reconciledCart.cart), proxySession);
|
|
49
|
+
}
|
|
50
|
+
const priorCart = {
|
|
51
|
+
amountMinor: proxySession.amountMinor,
|
|
52
|
+
cartSnapshot: proxySession.cartSnapshot,
|
|
53
|
+
cartVersion: proxySession.cartVersion,
|
|
54
|
+
currency: proxySession.currency,
|
|
55
|
+
};
|
|
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
|
+
};
|
|
75
|
+
cart = reconciledCart.cart;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
if (reconciledCart && options.validateCart) {
|
|
79
|
+
offer = await options.validateCart({ cart, session: proxySession });
|
|
80
|
+
}
|
|
81
|
+
const params = await options.buildCheckoutSessionParams({
|
|
82
|
+
cart,
|
|
83
|
+
offer,
|
|
84
|
+
proxySession,
|
|
85
|
+
stripe: options.stripe,
|
|
86
|
+
});
|
|
87
|
+
const checkoutSession = (await options.stripe.checkout.sessions.create(injectRequiredCheckoutSessionMetadata(params, proxySession.id), buildStripeRequestOptions(options, proxySession.id, psp)));
|
|
88
|
+
// Fail closed if the merchant forgot to place proxy_session_id metadata.
|
|
89
|
+
assertCheckoutSessionBelongsToProxy(checkoutSession, proxySession.id);
|
|
90
|
+
const binding = await recordOrReconcileProviderBinding({
|
|
91
|
+
checkoutSessionId: checkoutSession.id,
|
|
92
|
+
proxy: options.proxy,
|
|
93
|
+
proxySessionId: proxySession.id,
|
|
94
|
+
psp,
|
|
95
|
+
requestId: options.requestId,
|
|
96
|
+
stripe: options.stripe,
|
|
97
|
+
stripeRequestOptions: options.stripeRequestOptions,
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
binding,
|
|
101
|
+
cart,
|
|
102
|
+
checkoutSession,
|
|
103
|
+
clientSecret: checkoutSession.client_secret ?? null,
|
|
104
|
+
offer,
|
|
105
|
+
proxySession,
|
|
106
|
+
proxySessionId: proxySession.id,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (cause) {
|
|
110
|
+
if (rollbackCart) {
|
|
111
|
+
await rollbackReconciledCart({
|
|
112
|
+
cause,
|
|
113
|
+
proxy: options.proxy,
|
|
114
|
+
proxySessionId: proxySession.id,
|
|
115
|
+
requestId: options.requestId,
|
|
116
|
+
rollbackCart,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
throw cause;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function buildStripeRequestOptions(options, proxySessionId, psp) {
|
|
123
|
+
return {
|
|
124
|
+
...options.stripeRequestOptions,
|
|
125
|
+
idempotencyKey: `proxy-open-checkout:${proxySessionId}:${psp}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function assertExistingBindingMatches(proxySession, psp) {
|
|
129
|
+
if (proxySession.providerCheckoutSessionId === null) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (proxySession.providerCheckoutSessionPsp !== psp) {
|
|
133
|
+
throw new ProxyCheckoutValidationError(`Proxy session ${proxySession.id} is already bound under psp ${proxySession.providerCheckoutSessionPsp}.`, { code: "provider_binding_mismatch", field: "provider_checkout_session_psp" });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function bindingFromSession(proxySession) {
|
|
137
|
+
if (proxySession.providerCheckoutSessionId === null ||
|
|
138
|
+
proxySession.providerCheckoutSessionPsp === null) {
|
|
139
|
+
throw new Error("Proxy session is not bound to a provider checkout session.");
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
providerCheckoutSessionId: proxySession.providerCheckoutSessionId,
|
|
143
|
+
proxySessionId: proxySession.id,
|
|
144
|
+
psp: proxySession.providerCheckoutSessionPsp,
|
|
145
|
+
updatedAt: proxySession.updatedAt,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function rollbackReconciledCart(input) {
|
|
149
|
+
try {
|
|
150
|
+
await input.proxy.sessions.cart.set(input.proxySessionId, {
|
|
151
|
+
amountMinor: input.rollbackCart.amountMinor,
|
|
152
|
+
cartSnapshot: input.rollbackCart.cartSnapshot,
|
|
153
|
+
currency: input.rollbackCart.currency,
|
|
154
|
+
expectedCartVersion: input.rollbackCart.expectedCartVersion,
|
|
155
|
+
requestId: input.requestId,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (rollbackCause) {
|
|
159
|
+
throw new ProxyStripeCartSyncError("openCheckout failed after reconciling the Proxy cart, and the rollback failed; carts may be out of sync.", {
|
|
160
|
+
cause: input.cause,
|
|
161
|
+
code: "reconciliation_failed",
|
|
162
|
+
reconciled: false,
|
|
163
|
+
rollbackCause,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function recordOrReconcileProviderBinding(input) {
|
|
168
|
+
try {
|
|
169
|
+
return await input.proxy.sessions.recordProviderBinding(input.proxySessionId, {
|
|
170
|
+
providerCheckoutSessionId: input.checkoutSessionId,
|
|
171
|
+
psp: input.psp,
|
|
172
|
+
requestId: input.requestId,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch (cause) {
|
|
176
|
+
const reconciled = await readReconciledBinding(input).catch(() => undefined);
|
|
177
|
+
if (reconciled) {
|
|
178
|
+
return reconciled;
|
|
179
|
+
}
|
|
180
|
+
if (isProxyConflict(cause)) {
|
|
181
|
+
throw cause;
|
|
182
|
+
}
|
|
183
|
+
throw new ProxyStripeOpenCheckoutError("Stripe Checkout Session was created but Proxy provider binding could not be recorded.", {
|
|
184
|
+
cause,
|
|
185
|
+
code: "provider_binding_failed",
|
|
186
|
+
providerCheckoutSessionId: input.checkoutSessionId,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function readReconciledBinding(input) {
|
|
191
|
+
const current = await input.proxy.sessions.retrieve(input.proxySessionId, {
|
|
192
|
+
requestId: input.requestId,
|
|
193
|
+
});
|
|
194
|
+
if (current.providerCheckoutSessionId !== input.checkoutSessionId ||
|
|
195
|
+
current.providerCheckoutSessionPsp !== input.psp) {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
const checkoutSession = await input.stripe.checkout.sessions.retrieve(input.checkoutSessionId, undefined, input.stripeRequestOptions);
|
|
199
|
+
assertCheckoutSessionBelongsToProxy(checkoutSession, input.proxySessionId);
|
|
200
|
+
return bindingFromSession(current);
|
|
201
|
+
}
|
|
202
|
+
function isProxyConflict(error) {
|
|
203
|
+
return (typeof error === "object" &&
|
|
204
|
+
error !== null &&
|
|
205
|
+
"statusCode" in error &&
|
|
206
|
+
error.statusCode === 409);
|
|
207
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider (psp) identifier normalization, kept in lockstep with the Proxy API.
|
|
3
|
+
*
|
|
4
|
+
* The API normalizes the psp before persisting a provider binding
|
|
5
|
+
* (`psp.trim().toLowerCase()`), then compares stored bindings against that
|
|
6
|
+
* normalized value. The SDK must normalize identically before recording or
|
|
7
|
+
* validating a binding so a caller passing the same psp with different
|
|
8
|
+
* casing/whitespace for `openCheckout` vs `syncCheckoutCart` does not get a
|
|
9
|
+
* false `provider_binding_mismatch`.
|
|
10
|
+
*/
|
|
11
|
+
/** Normalize and validate a psp identifier exactly as the Proxy API does before storing/comparing a binding. */
|
|
12
|
+
export declare function normalizePsp(psp: string): string;
|
package/dist/esm/psp.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ProxyCheckoutValidationError } from "@proxy-checkout/server-js";
|
|
2
|
+
/**
|
|
3
|
+
* Provider (psp) identifier normalization, kept in lockstep with the Proxy API.
|
|
4
|
+
*
|
|
5
|
+
* The API normalizes the psp before persisting a provider binding
|
|
6
|
+
* (`psp.trim().toLowerCase()`), then compares stored bindings against that
|
|
7
|
+
* normalized value. The SDK must normalize identically before recording or
|
|
8
|
+
* validating a binding so a caller passing the same psp with different
|
|
9
|
+
* casing/whitespace for `openCheckout` vs `syncCheckoutCart` does not get a
|
|
10
|
+
* false `provider_binding_mismatch`.
|
|
11
|
+
*/
|
|
12
|
+
/** Normalize and validate a psp identifier exactly as the Proxy API does before storing/comparing a binding. */
|
|
13
|
+
export function normalizePsp(psp) {
|
|
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;
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `syncCheckoutCart` — coordinated Proxy + Stripe cart update.
|
|
3
|
+
*
|
|
4
|
+
* Validates ownership against the first-class Proxy provider binding (not just
|
|
5
|
+
* Stripe metadata), then updates the Proxy cart and the Stripe checkout in the
|
|
6
|
+
* safest order with deterministic rollback/reconciliation on failure.
|
|
7
|
+
*/
|
|
8
|
+
import { type JsonObject, type MerchantProxySession, type ProxyCartValidator, type ProxyCheckoutServerClient } from "@proxy-checkout/server-js";
|
|
9
|
+
import type { StripeCheckoutSessionLike, StripeClientLike, StripeRequestOptionsWithoutIdempotencyLike } from "./types.js";
|
|
10
|
+
/** The next cart, expressed both as Proxy state and the typed merchant cart. */
|
|
11
|
+
export interface SyncedCart<TCart> {
|
|
12
|
+
readonly amountMinor: number;
|
|
13
|
+
readonly cart: TCart;
|
|
14
|
+
readonly cartSnapshot: JsonObject;
|
|
15
|
+
readonly currency: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SyncCheckoutCartOptions<TStripe extends StripeClientLike, TCart, TInput> {
|
|
18
|
+
/** Compute the next cart from current state and the merchant input. */
|
|
19
|
+
readonly buildNextCart: (input: {
|
|
20
|
+
currentCart: TCart;
|
|
21
|
+
currentSession: MerchantProxySession;
|
|
22
|
+
input: TInput;
|
|
23
|
+
}) => Promise<SyncedCart<TCart>> | SyncedCart<TCart>;
|
|
24
|
+
/** Derive the Stripe `line_items` update from the next typed cart. */
|
|
25
|
+
readonly buildStripeLineItems: (cart: TCart) => unknown;
|
|
26
|
+
/** Optional schema validating the current cart snapshot into a typed cart. */
|
|
27
|
+
readonly cartSchema?: ProxyCartValidator<TCart>;
|
|
28
|
+
readonly checkoutSessionId: string;
|
|
29
|
+
/** Merchant-supplied change request (e.g. new interval/quantity). */
|
|
30
|
+
readonly input: TInput;
|
|
31
|
+
readonly proxy: ProxyCheckoutServerClient;
|
|
32
|
+
readonly proxySessionId: string;
|
|
33
|
+
/** Provider identifier the session must be bound under. Defaults to "stripe". */
|
|
34
|
+
readonly psp?: string;
|
|
35
|
+
readonly requestId?: string;
|
|
36
|
+
readonly stripe: TStripe;
|
|
37
|
+
/** Optional Stripe request options, e.g. Stripe Connect account scoping. */
|
|
38
|
+
readonly stripeRequestOptions?: StripeRequestOptionsWithoutIdempotencyLike;
|
|
39
|
+
}
|
|
40
|
+
export interface SyncCheckoutCartResult<TCart> {
|
|
41
|
+
readonly amountMinor: number;
|
|
42
|
+
readonly cart: TCart;
|
|
43
|
+
readonly cartSnapshot: JsonObject;
|
|
44
|
+
readonly checkoutSession: StripeCheckoutSessionLike;
|
|
45
|
+
readonly currency: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function syncCheckoutCart<TStripe extends StripeClientLike, TCart, TInput>(options: SyncCheckoutCartOptions<TStripe, TCart, TInput>): Promise<SyncCheckoutCartResult<TCart>>;
|
|
@@ -8,15 +8,19 @@
|
|
|
8
8
|
import { ProxyCheckoutValidationError, } from "@proxy-checkout/server-js";
|
|
9
9
|
import { ProxyStripeCartSyncError } from "./errors.js";
|
|
10
10
|
import { assertCheckoutSessionBelongsToProxy } from "./metadata.js";
|
|
11
|
+
import { normalizePsp } from "./psp.js";
|
|
11
12
|
export async function syncCheckoutCart(options) {
|
|
12
|
-
|
|
13
|
+
// Normalize the same way the API does on store, so the binding comparison is
|
|
14
|
+
// normalized-to-normalized and a custom psp with differing casing/whitespace
|
|
15
|
+
// does not produce a false provider_binding_mismatch.
|
|
16
|
+
const psp = normalizePsp(options.psp ?? "stripe");
|
|
13
17
|
const session = await options.proxy.sessions.retrieve(options.proxySessionId, {
|
|
14
18
|
requestId: options.requestId,
|
|
15
19
|
});
|
|
16
20
|
// Ownership: validate against the stored binding first, then defensively
|
|
17
21
|
// against the Stripe object's own metadata.
|
|
18
22
|
assertSessionBoundTo(session, options.checkoutSessionId, psp);
|
|
19
|
-
const stripeSession = await options.stripe.checkout.sessions.retrieve(options.checkoutSessionId);
|
|
23
|
+
const stripeSession = await options.stripe.checkout.sessions.retrieve(options.checkoutSessionId, undefined, options.stripeRequestOptions);
|
|
20
24
|
assertCheckoutSessionBelongsToProxy(stripeSession, options.proxySessionId);
|
|
21
25
|
const currentCart = options.cartSchema
|
|
22
26
|
? options.proxy.sessions.parseCart(session, options.cartSchema)
|
|
@@ -29,41 +33,57 @@ export async function syncCheckoutCart(options) {
|
|
|
29
33
|
const prior = {
|
|
30
34
|
amountMinor: session.amountMinor,
|
|
31
35
|
cartSnapshot: session.cartSnapshot,
|
|
36
|
+
cartVersion: session.cartVersion,
|
|
32
37
|
currency: session.currency,
|
|
33
38
|
};
|
|
39
|
+
const stripeUpdateParams = {
|
|
40
|
+
line_items: options.buildStripeLineItems(next.cart),
|
|
41
|
+
};
|
|
34
42
|
// Update Proxy first; the API rejects a non-editable session before Stripe is touched.
|
|
35
|
-
await options.proxy.sessions.cart.set(options.proxySessionId, {
|
|
43
|
+
const proxyCartUpdate = await options.proxy.sessions.cart.set(options.proxySessionId, {
|
|
36
44
|
amountMinor: next.amountMinor,
|
|
37
45
|
cartSnapshot: next.cartSnapshot,
|
|
38
46
|
currency: next.currency,
|
|
47
|
+
expectedCartVersion: session.cartVersion,
|
|
39
48
|
requestId: options.requestId,
|
|
40
49
|
});
|
|
50
|
+
const stripeUpdateOptions = buildStripeUpdateRequestOptions(options, proxyCartUpdate.cartVersion);
|
|
41
51
|
let updatedCheckout;
|
|
42
52
|
try {
|
|
43
|
-
updatedCheckout = await options.stripe.checkout.sessions.update(options.checkoutSessionId,
|
|
44
|
-
line_items: options.buildStripeLineItems(next.cart),
|
|
45
|
-
});
|
|
53
|
+
updatedCheckout = await options.stripe.checkout.sessions.update(options.checkoutSessionId, stripeUpdateParams, stripeUpdateOptions);
|
|
46
54
|
}
|
|
47
|
-
catch
|
|
55
|
+
catch {
|
|
48
56
|
try {
|
|
49
|
-
await options.
|
|
50
|
-
amountMinor: prior.amountMinor,
|
|
51
|
-
cartSnapshot: prior.cartSnapshot,
|
|
52
|
-
currency: prior.currency,
|
|
53
|
-
requestId: options.requestId,
|
|
54
|
-
});
|
|
57
|
+
updatedCheckout = await options.stripe.checkout.sessions.update(options.checkoutSessionId, stripeUpdateParams, stripeUpdateOptions);
|
|
55
58
|
}
|
|
56
|
-
catch (
|
|
57
|
-
|
|
59
|
+
catch (retryCause) {
|
|
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 });
|
|
58
73
|
}
|
|
59
|
-
throw new ProxyStripeCartSyncError("Stripe checkout update failed; the Proxy cart was rolled back to its prior state.", { cause, code: "stripe_update_failed", reconciled: true });
|
|
60
74
|
}
|
|
61
75
|
return {
|
|
62
76
|
amountMinor: next.amountMinor,
|
|
63
77
|
cart: next.cart,
|
|
64
78
|
cartSnapshot: next.cartSnapshot,
|
|
65
79
|
checkoutSession: updatedCheckout,
|
|
66
|
-
currency:
|
|
80
|
+
currency: proxyCartUpdate.currency,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function buildStripeUpdateRequestOptions(options, cartVersion) {
|
|
84
|
+
return {
|
|
85
|
+
...options.stripeRequestOptions,
|
|
86
|
+
idempotencyKey: `proxy-sync-checkout-cart:${options.proxySessionId}:${options.checkoutSessionId}:${cartVersion}`,
|
|
67
87
|
};
|
|
68
88
|
}
|
|
69
89
|
function assertSessionBoundTo(session, checkoutSessionId, psp) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural Stripe types.
|
|
3
|
+
*
|
|
4
|
+
* The adapter is dependency-free: merchants inject their own `Stripe` instance
|
|
5
|
+
* and objects, which are structurally compatible with these narrow interfaces.
|
|
6
|
+
* This avoids a runtime/build dependency on the `stripe` package while keeping
|
|
7
|
+
* the bits the adapter actually touches type-safe.
|
|
8
|
+
*/
|
|
9
|
+
export type StripeMetadataValue = string | number | null;
|
|
10
|
+
export type StripeMetadata = Record<string, StripeMetadataValue> | null | undefined;
|
|
11
|
+
export type StripeMetadataParam = Record<string, StripeMetadataValue> | undefined;
|
|
12
|
+
export interface StripeCheckoutSessionLike {
|
|
13
|
+
readonly client_secret?: string | null;
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly metadata?: StripeMetadata;
|
|
16
|
+
}
|
|
17
|
+
export interface StripeSubscriptionLike {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly metadata?: StripeMetadata;
|
|
20
|
+
}
|
|
21
|
+
export interface StripePaymentIntentLike {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly metadata?: StripeMetadata;
|
|
24
|
+
}
|
|
25
|
+
export interface StripeCheckoutSessionUpdateParams {
|
|
26
|
+
readonly line_items?: unknown;
|
|
27
|
+
readonly metadata?: StripeMetadataParam;
|
|
28
|
+
}
|
|
29
|
+
export type StripeCheckoutSessionCreateParams = object & {
|
|
30
|
+
readonly metadata?: StripeMetadataParam;
|
|
31
|
+
readonly mode?: "payment" | "setup" | "subscription";
|
|
32
|
+
readonly payment_intent_data?: unknown;
|
|
33
|
+
readonly subscription_data?: unknown;
|
|
34
|
+
};
|
|
35
|
+
export interface StripeRequestOptionsLike {
|
|
36
|
+
readonly idempotencyKey?: string;
|
|
37
|
+
readonly [key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface StripeRequestOptionsWithoutIdempotencyLike {
|
|
40
|
+
readonly idempotencyKey?: never;
|
|
41
|
+
readonly [key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
export interface StripeCheckoutSessionsApiLike {
|
|
44
|
+
create(params: unknown, options?: unknown): Promise<StripeCheckoutSessionLike>;
|
|
45
|
+
retrieve(id: string, params?: unknown, options?: unknown): Promise<StripeCheckoutSessionLike>;
|
|
46
|
+
update(id: string, params: unknown, options?: unknown): Promise<StripeCheckoutSessionLike>;
|
|
47
|
+
}
|
|
48
|
+
export interface StripeClientLike {
|
|
49
|
+
readonly checkout: {
|
|
50
|
+
readonly sessions: StripeCheckoutSessionsApiLike;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const proxyCheckoutStripeServerSdkName = "@proxy-checkout/stripe-server-js";
|
|
2
|
-
export const proxyCheckoutStripeServerSdkVersion = "0.0.
|
|
2
|
+
export const proxyCheckoutStripeServerSdkVersion = "0.0.1";
|