@proxy-checkout/server-js 0.0.3 → 0.0.4-pr-76.13.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/dist/cart.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Typed cart-snapshot validation.
3
+ *
4
+ * The cart snapshot is merchant-defined and opaque to Proxy, so the SDK keeps
5
+ * the shape app-owned but standardizes the parse step: pass a zod-style schema
6
+ * (or a plain validator function) and get a typed cart or a structured error.
7
+ */
8
+ /** A zod-style schema (`{ parse }`) or a plain validator function. */
9
+ export type ProxyCartValidator<TCart> = {
10
+ parse: (value: unknown) => TCart;
11
+ } | ((value: unknown) => TCart);
12
+ /** Run a cart snapshot through a validator, wrapping failures in a structured SDK error. */
13
+ export declare function parseProxyCart<TCart>(cartSnapshot: unknown, validator: ProxyCartValidator<TCart>): TCart;
14
+ /** Optional extraction of a buyer reference embedded in the typed cart, for consistency checks. */
15
+ export interface ProxyCartParseOptions<TCart> {
16
+ readonly getBuyerReference?: (cart: TCart) => string | null | undefined;
17
+ }
package/dist/cart.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Typed cart-snapshot validation.
3
+ *
4
+ * The cart snapshot is merchant-defined and opaque to Proxy, so the SDK keeps
5
+ * the shape app-owned but standardizes the parse step: pass a zod-style schema
6
+ * (or a plain validator function) and get a typed cart or a structured error.
7
+ */
8
+ import { ProxyCheckoutValidationError } from "./errors.js";
9
+ /** Run a cart snapshot through a validator, wrapping failures in a structured SDK error. */
10
+ export function parseProxyCart(cartSnapshot, validator) {
11
+ try {
12
+ return typeof validator === "function"
13
+ ? validator(cartSnapshot)
14
+ : validator.parse(cartSnapshot);
15
+ }
16
+ catch (cause) {
17
+ throw new ProxyCheckoutValidationError("Proxy cart snapshot failed validation.", {
18
+ cause,
19
+ code: "cart_invalid",
20
+ field: "cart_snapshot",
21
+ });
22
+ }
23
+ }
package/dist/client.d.ts CHANGED
@@ -1,11 +1,18 @@
1
+ import { ProxyEventsResource } from "./events.js";
1
2
  import { ProxySessionsResource } from "./sessions.js";
2
3
  import { ProxySubscriptionsResource } from "./subscriptions.js";
3
4
  import type { ProxyCheckoutServerClientOptions } from "./types.js";
5
+ import { ProxyWebhooksResource } from "./webhook-handler.js";
4
6
  import { ProxyWebhookEndpointsResource } from "./webhooks.js";
5
7
  export declare class ProxyCheckoutServerClient {
6
8
  readonly apiHost: string;
7
9
  readonly sessions: ProxySessionsResource;
8
10
  readonly subscriptions: ProxySubscriptionsResource;
11
+ /** Current-state lifecycle resolver for signed webhook events. */
12
+ readonly events: ProxyEventsResource;
13
+ /** Webhook delivery handling: `handle`, `process`, `constructEvent`. */
14
+ readonly webhooks: ProxyWebhooksResource;
15
+ /** Webhook endpoint management API (create/list/rotate). */
9
16
  readonly webhookEndpoints: ProxyWebhookEndpointsResource;
10
17
  constructor(options: ProxyCheckoutServerClientOptions);
11
18
  }
package/dist/client.js CHANGED
@@ -1,11 +1,18 @@
1
+ import { ProxyEventsResource } from "./events.js";
1
2
  import { ProxyCheckoutHttpClient } from "./http-client.js";
2
3
  import { ProxySessionsResource } from "./sessions.js";
3
4
  import { ProxySubscriptionsResource } from "./subscriptions.js";
5
+ import { ProxyWebhooksResource } from "./webhook-handler.js";
4
6
  import { ProxyWebhookEndpointsResource } from "./webhooks.js";
5
7
  export class ProxyCheckoutServerClient {
6
8
  apiHost;
7
9
  sessions;
8
10
  subscriptions;
11
+ /** Current-state lifecycle resolver for signed webhook events. */
12
+ events;
13
+ /** Webhook delivery handling: `handle`, `process`, `constructEvent`. */
14
+ webhooks;
15
+ /** Webhook endpoint management API (create/list/rotate). */
9
16
  webhookEndpoints;
10
17
  constructor(options) {
11
18
  const httpClient = new ProxyCheckoutHttpClient({
@@ -18,7 +25,9 @@ export class ProxyCheckoutServerClient {
18
25
  payHost: options.payHost,
19
26
  publishableKey: options.publishableKey,
20
27
  });
21
- this.subscriptions = new ProxySubscriptionsResource(httpClient);
28
+ this.subscriptions = new ProxySubscriptionsResource(httpClient, this.sessions);
29
+ this.events = new ProxyEventsResource(this.sessions, this.subscriptions);
30
+ this.webhooks = new ProxyWebhooksResource(this.events);
22
31
  this.webhookEndpoints = new ProxyWebhookEndpointsResource(httpClient);
23
32
  }
24
33
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Buyer-reference and session linkage consistency checks.
3
+ *
4
+ * Proxy current-state retrieval is the source of truth, but the SDK still
5
+ * guards that the records it stitches together actually belong to the same
6
+ * buyer and session before a merchant acts on them.
7
+ */
8
+ import type { MerchantProxySession } from "./sessions.js";
9
+ import type { MerchantProxySubscription } from "./subscriptions.js";
10
+ /**
11
+ * Assert that a subscription belongs to the given original session and that the
12
+ * buyer references agree. Throws {@link ProxyCheckoutValidationError} otherwise.
13
+ */
14
+ export declare function assertSubscriptionMatchesSession(subscription: Pick<MerchantProxySubscription, "buyerReference" | "id" | "originalProxySessionId">, session: Pick<MerchantProxySession, "buyerReference" | "id">): void;
15
+ /**
16
+ * Assert that a cart-embedded buyer reference, when present, matches the session
17
+ * buyer reference. A `null`/`undefined` cart buyer reference is allowed (the
18
+ * cart simply does not carry one).
19
+ */
20
+ export declare function assertCartBuyerReference(cartBuyerReference: string | null | undefined, session: Pick<MerchantProxySession, "buyerReference" | "id">): void;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Buyer-reference and session linkage consistency checks.
3
+ *
4
+ * Proxy current-state retrieval is the source of truth, but the SDK still
5
+ * guards that the records it stitches together actually belong to the same
6
+ * buyer and session before a merchant acts on them.
7
+ */
8
+ import { ProxyCheckoutValidationError } from "./errors.js";
9
+ /**
10
+ * Assert that a subscription belongs to the given original session and that the
11
+ * buyer references agree. Throws {@link ProxyCheckoutValidationError} otherwise.
12
+ */
13
+ export function assertSubscriptionMatchesSession(subscription, session) {
14
+ if (subscription.originalProxySessionId !== session.id) {
15
+ throw new ProxyCheckoutValidationError(`Proxy subscription ${subscription.id} is not linked to session ${session.id}.`, { code: "subscription_session_mismatch", field: "original_proxy_session_id" });
16
+ }
17
+ if (subscription.buyerReference !== session.buyerReference) {
18
+ throw new ProxyCheckoutValidationError(`Proxy subscription ${subscription.id} buyer reference does not match session ${session.id}.`, { code: "buyer_reference_mismatch", field: "buyer_reference" });
19
+ }
20
+ }
21
+ /**
22
+ * Assert that a cart-embedded buyer reference, when present, matches the session
23
+ * buyer reference. A `null`/`undefined` cart buyer reference is allowed (the
24
+ * cart simply does not carry one).
25
+ */
26
+ export function assertCartBuyerReference(cartBuyerReference, session) {
27
+ if (cartBuyerReference !== null &&
28
+ cartBuyerReference !== undefined &&
29
+ cartBuyerReference !== session.buyerReference) {
30
+ throw new ProxyCheckoutValidationError(`Proxy session ${session.id} cart buyer reference does not match the session buyer reference.`, { code: "buyer_reference_mismatch", field: "cart.buyerReference" });
31
+ }
32
+ }
package/dist/errors.d.ts CHANGED
@@ -4,6 +4,23 @@ export interface ProxyCheckoutApiErrorDetails {
4
4
  readonly responseBody: unknown;
5
5
  readonly statusCode: number;
6
6
  }
7
+ export type ProxyCheckoutValidationCode = "buyer_reference_mismatch" | "cart_invalid" | "invalid_date" | "provider_binding_mismatch" | "subscription_session_mismatch";
8
+ /**
9
+ * Raised when the SDK receives Proxy data it cannot safely normalize, for
10
+ * example an invalid date string, a cart snapshot that fails the merchant
11
+ * schema, or a buyer-reference / provider-binding consistency violation.
12
+ */
13
+ export declare class ProxyCheckoutValidationError extends Error {
14
+ readonly name = "ProxyCheckoutValidationError";
15
+ readonly code: ProxyCheckoutValidationCode;
16
+ readonly field: string | undefined;
17
+ readonly cause: unknown;
18
+ constructor(message: string, details: {
19
+ code: ProxyCheckoutValidationCode;
20
+ field?: string;
21
+ cause?: unknown;
22
+ });
23
+ }
7
24
  export declare class ProxyCheckoutApiError extends Error {
8
25
  readonly name = "ProxyCheckoutApiError";
9
26
  readonly code: string | undefined;
package/dist/errors.js CHANGED
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Raised when the SDK receives Proxy data it cannot safely normalize, for
3
+ * example an invalid date string, a cart snapshot that fails the merchant
4
+ * schema, or a buyer-reference / provider-binding consistency violation.
5
+ */
6
+ export class ProxyCheckoutValidationError extends Error {
7
+ name = "ProxyCheckoutValidationError";
8
+ code;
9
+ field;
10
+ cause;
11
+ constructor(message, details) {
12
+ super(message);
13
+ this.code = details.code;
14
+ this.field = details.field;
15
+ this.cause = details.cause;
16
+ }
17
+ }
1
18
  export class ProxyCheckoutApiError extends Error {
2
19
  name = "ProxyCheckoutApiError";
3
20
  code;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Current-state lifecycle resolver for signed Proxy webhook events.
3
+ *
4
+ * A webhook is only a wakeup: this resolver re-reads the authoritative session
5
+ * and subscription state with the merchant secret key and returns a typed
6
+ * lifecycle decision. It never grants from webhook payload fields, and it
7
+ * classifies subscription events from current state rather than event order.
8
+ */
9
+ import type { MerchantProxySession, ProxySessionsResource } from "./sessions.js";
10
+ import type { MerchantProxySubscription, ProxySubscriptionsResource } from "./subscriptions.js";
11
+ import type { ProxyCheckoutServerRequestOptions } from "./types.js";
12
+ import type { ProxyWebhookEvent } from "./webhooks.js";
13
+ export interface ResolveProxyEventOptions extends ProxyCheckoutServerRequestOptions {
14
+ }
15
+ interface ResolvedBase {
16
+ readonly event: ProxyWebhookEvent;
17
+ }
18
+ /**
19
+ * First paid/provisionable wakeup for a session. `subscription` is present when
20
+ * the event carried a subscription id; otherwise the merchant computes initial
21
+ * access from its own cart policy (`accessEndsAt`/`willRenew` are then null).
22
+ */
23
+ export interface ResolvedInitialProvision extends ResolvedBase {
24
+ readonly accessEndsAt: Date | null;
25
+ readonly kind: "initial_provision";
26
+ readonly session: MerchantProxySession;
27
+ readonly subscription: MerchantProxySubscription | null;
28
+ readonly willRenew: boolean | null;
29
+ }
30
+ export interface ResolvedSubscriptionRenewed extends ResolvedBase {
31
+ readonly accessEndsAt: Date | null;
32
+ readonly kind: "subscription_renewed";
33
+ readonly session: MerchantProxySession;
34
+ readonly subscription: MerchantProxySubscription;
35
+ readonly willRenew: boolean;
36
+ }
37
+ export interface ResolvedSubscriptionCancelScheduled extends ResolvedBase {
38
+ readonly accessEndsAt: Date | null;
39
+ readonly kind: "subscription_cancel_scheduled";
40
+ readonly session: MerchantProxySession;
41
+ readonly subscription: MerchantProxySubscription;
42
+ readonly willRenew: false;
43
+ }
44
+ export interface ResolvedSubscriptionCancelled extends ResolvedBase {
45
+ readonly accessEndsAt: Date | null;
46
+ readonly kind: "subscription_cancelled";
47
+ readonly session: MerchantProxySession;
48
+ readonly subscription: MerchantProxySubscription;
49
+ readonly willRenew: false;
50
+ }
51
+ export interface ResolvedPaymentRisk extends ResolvedBase {
52
+ readonly accessEndsAt: Date | null;
53
+ readonly kind: "payment_risk";
54
+ readonly latestPaymentStatus: string | null;
55
+ readonly session: MerchantProxySession;
56
+ readonly subscription: MerchantProxySubscription;
57
+ readonly willRenew: boolean;
58
+ }
59
+ export interface ResolvedTerminalSession extends ResolvedBase {
60
+ readonly kind: "terminal_session";
61
+ readonly session: MerchantProxySession;
62
+ }
63
+ export interface ResolvedIgnored extends ResolvedBase {
64
+ readonly kind: "ignored";
65
+ readonly reason: string;
66
+ readonly sessionId: string | null;
67
+ readonly subscriptionId: string | null;
68
+ }
69
+ export type ResolvedProxyEvent = ResolvedInitialProvision | ResolvedSubscriptionRenewed | ResolvedSubscriptionCancelScheduled | ResolvedSubscriptionCancelled | ResolvedPaymentRisk | ResolvedTerminalSession | ResolvedIgnored;
70
+ export type ResolvedProxyEventKind = ResolvedProxyEvent["kind"];
71
+ export declare class ProxyEventsResource {
72
+ private readonly sessions;
73
+ private readonly subscriptions;
74
+ constructor(sessions: ProxySessionsResource, subscriptions: ProxySubscriptionsResource);
75
+ /**
76
+ * Resolve a signed webhook event into a current-state lifecycle decision by
77
+ * re-reading the authoritative session/subscription with the secret key.
78
+ */
79
+ resolveCurrentState(event: ProxyWebhookEvent, options?: ResolveProxyEventOptions): Promise<ResolvedProxyEvent>;
80
+ private resolveSessionEvent;
81
+ private resolveSubscriptionEvent;
82
+ }
83
+ export {};
package/dist/events.js ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Current-state lifecycle resolver for signed Proxy webhook events.
3
+ *
4
+ * A webhook is only a wakeup: this resolver re-reads the authoritative session
5
+ * and subscription state with the merchant secret key and returns a typed
6
+ * lifecycle decision. It never grants from webhook payload fields, and it
7
+ * classifies subscription events from current state rather than event order.
8
+ */
9
+ import { assertSubscriptionMatchesSession } from "./consistency.js";
10
+ import { isSessionProvisionable, isSessionTerminal, isSubscriptionEnded, isSubscriptionPaymentAtRisk, subscriptionAccessEndsAt, subscriptionWillRenew, } from "./lifecycle.js";
11
+ import { isProxySessionEvent, isProxySubscriptionEvent } from "./webhook-events.js";
12
+ export class ProxyEventsResource {
13
+ sessions;
14
+ subscriptions;
15
+ constructor(sessions, subscriptions) {
16
+ this.sessions = sessions;
17
+ this.subscriptions = subscriptions;
18
+ }
19
+ /**
20
+ * Resolve a signed webhook event into a current-state lifecycle decision by
21
+ * re-reading the authoritative session/subscription with the secret key.
22
+ */
23
+ async resolveCurrentState(event, options = {}) {
24
+ if (isProxySubscriptionEvent(event.type)) {
25
+ return this.resolveSubscriptionEvent(event, options);
26
+ }
27
+ if (isProxySessionEvent(event.type)) {
28
+ return this.resolveSessionEvent(event, options);
29
+ }
30
+ return ignored(event, `Event type ${event.type} is not a lifecycle event.`, {
31
+ sessionId: readEventString(event.data.proxy_session_id),
32
+ subscriptionId: readEventString(event.data.subscription_id),
33
+ });
34
+ }
35
+ async resolveSessionEvent(event, options) {
36
+ const sessionId = extractSessionId(event);
37
+ if (sessionId === null) {
38
+ return ignored(event, "Session event is missing proxy_session_id.", {
39
+ sessionId: null,
40
+ subscriptionId: readEventString(event.data.subscription_id),
41
+ });
42
+ }
43
+ const session = await this.sessions.retrieve(sessionId, options);
44
+ if (isSessionProvisionable(session)) {
45
+ const subscriptionId = readEventString(event.data.subscription_id);
46
+ let subscription = null;
47
+ if (subscriptionId !== null) {
48
+ subscription = await this.subscriptions.retrieve(subscriptionId, options);
49
+ assertSubscriptionMatchesSession(subscription, session);
50
+ }
51
+ return {
52
+ accessEndsAt: subscription === null ? null : subscriptionAccessEndsAt(subscription),
53
+ event,
54
+ kind: "initial_provision",
55
+ session,
56
+ subscription,
57
+ willRenew: subscription === null ? null : subscriptionWillRenew(subscription),
58
+ };
59
+ }
60
+ if (isSessionTerminal(session)) {
61
+ return { event, kind: "terminal_session", session };
62
+ }
63
+ return ignored(event, `Session ${session.id} status ${session.status} is not yet actionable.`, {
64
+ sessionId: session.id,
65
+ subscriptionId: null,
66
+ });
67
+ }
68
+ async resolveSubscriptionEvent(event, options) {
69
+ const subscriptionId = readEventString(event.data.subscription_id);
70
+ if (subscriptionId === null) {
71
+ return ignored(event, "Subscription event is missing subscription_id.", {
72
+ sessionId: readEventString(event.data.proxy_session_id),
73
+ subscriptionId: null,
74
+ });
75
+ }
76
+ const subscription = await this.subscriptions.retrieve(subscriptionId, options);
77
+ const session = await this.sessions.retrieve(subscription.originalProxySessionId, options);
78
+ assertSubscriptionMatchesSession(subscription, session);
79
+ const accessEndsAt = subscriptionAccessEndsAt(subscription);
80
+ if (isSubscriptionEnded(subscription)) {
81
+ return {
82
+ accessEndsAt,
83
+ event,
84
+ kind: "subscription_cancelled",
85
+ session,
86
+ subscription,
87
+ willRenew: false,
88
+ };
89
+ }
90
+ if (subscription.cancelAtPeriodEnd) {
91
+ return {
92
+ accessEndsAt,
93
+ event,
94
+ kind: "subscription_cancel_scheduled",
95
+ session,
96
+ subscription,
97
+ willRenew: false,
98
+ };
99
+ }
100
+ if (isSubscriptionPaymentAtRisk(subscription)) {
101
+ return {
102
+ accessEndsAt,
103
+ event,
104
+ kind: "payment_risk",
105
+ latestPaymentStatus: subscription.latestPaymentStatus,
106
+ session,
107
+ subscription,
108
+ willRenew: subscriptionWillRenew(subscription),
109
+ };
110
+ }
111
+ return {
112
+ accessEndsAt,
113
+ event,
114
+ kind: "subscription_renewed",
115
+ session,
116
+ subscription,
117
+ willRenew: subscriptionWillRenew(subscription),
118
+ };
119
+ }
120
+ }
121
+ function ignored(event, reason, ids) {
122
+ return {
123
+ event,
124
+ kind: "ignored",
125
+ reason,
126
+ sessionId: ids.sessionId,
127
+ subscriptionId: ids.subscriptionId,
128
+ };
129
+ }
130
+ /** Read the session id from an event, handling the nested `proxy_session.expired` payload. */
131
+ function extractSessionId(event) {
132
+ const direct = readEventString(event.data.proxy_session_id);
133
+ if (direct !== null) {
134
+ return direct;
135
+ }
136
+ const nested = event.data.proxy_session;
137
+ if (isRecord(nested)) {
138
+ return readEventString(nested.id);
139
+ }
140
+ return null;
141
+ }
142
+ function readEventString(value) {
143
+ return typeof value === "string" && value.length > 0 ? value : null;
144
+ }
145
+ function isRecord(value) {
146
+ return typeof value === "object" && value !== null && !Array.isArray(value);
147
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,13 @@
1
+ export { type ProxyCartParseOptions, type ProxyCartValidator, parseProxyCart, } from "./cart.js";
1
2
  export { createProxyCheckoutServerClient, ProxyCheckoutServerClient, } from "./client.js";
2
- export { ProxyCheckoutApiError, type ProxyCheckoutApiErrorDetails } from "./errors.js";
3
- export { type CreateProxySessionHandoffInput, type CreateProxySessionInput, type CreateProxySessionOptions, type MerchantProxySession, type PayerHandoffResult, type PayerOpenedResult, type ProxySession, ProxySessionCartResource, type ProxySessionCartResult, type ProxySessionHandoff, ProxySessionsResource, proxyCheckoutServerEndpoints, type SetProxySessionCartInput, } from "./sessions.js";
4
- export { type MerchantProxySubscription, ProxySubscriptionsResource, proxyCheckoutSubscriptionEndpoints, } from "./subscriptions.js";
3
+ export { assertCartBuyerReference, assertSubscriptionMatchesSession, } from "./consistency.js";
4
+ export { ProxyCheckoutApiError, type ProxyCheckoutApiErrorDetails, type ProxyCheckoutValidationCode, ProxyCheckoutValidationError, } from "./errors.js";
5
+ export { ProxyEventsResource, type ResolvedIgnored, type ResolvedInitialProvision, type ResolvedPaymentRisk, type ResolvedProxyEvent, type ResolvedProxyEventKind, type ResolvedSubscriptionCancelled, type ResolvedSubscriptionCancelScheduled, type ResolvedSubscriptionRenewed, type ResolvedTerminalSession, type ResolveProxyEventOptions, } from "./events.js";
6
+ export { AT_RISK_SUBSCRIPTION_STATUSES, ENDED_SUBSCRIPTION_STATUSES, isSessionProvisionable, isSessionTerminal, isSubscriptionEnded, isSubscriptionPaymentAtRisk, PROVISIONABLE_SESSION_STATUSES, parseOptionalProxyDate, requireProxyDate, subscriptionAccessEndsAt, subscriptionWillRenew, TERMINAL_SESSION_STATUSES, } from "./lifecycle.js";
7
+ export { type CreateProxySessionHandoffInput, type CreateProxySessionInput, type CreateProxySessionOptions, type MerchantProxySession, type PayerHandoffResult, type PayerOpenedResult, type ProxySession, ProxySessionCartResource, type ProxySessionCartResult, type ProxySessionHandoff, type ProxySessionProviderBinding, ProxySessionsResource, proxyCheckoutServerEndpoints, type RecordProviderBindingInput, type SetProxySessionCartInput, type TypedMerchantProxySession, } from "./sessions.js";
8
+ export { type MerchantProxySubscription, ProxySubscriptionsResource, type ProxySubscriptionWithSession, type ProxySubscriptionWithUntypedSession, proxyCheckoutSubscriptionEndpoints, } from "./subscriptions.js";
5
9
  export type { JsonObject, JsonPrimitive, JsonValue, ProxyCheckoutFetch, ProxyCheckoutServerClientOptions, ProxyCheckoutServerRequestOptions, } from "./types.js";
6
10
  export { proxyCheckoutServerSdkName, proxyCheckoutServerSdkUserAgent, proxyCheckoutServerSdkVersion, } from "./version.js";
7
- export { type ConstructProxyWebhookEventInput, type CreateWebhookEndpointInput, constructProxyWebhookEvent, PROXY_SIGNATURE_HEADER, ProxyWebhookEndpointsResource, type ProxyWebhookEvent, proxyCheckoutWebhookEndpointEndpoints, type RotateWebhookEndpointSecretInput, type UpdateWebhookEndpointInput, type VerifyProxyWebhookSignatureInput, verifyProxyWebhookSignature, type WebhookEndpoint, type WebhookEndpointSecretResult, } from "./webhooks.js";
11
+ export { isProxyPaymentAttemptEvent, isProxySessionEvent, isProxySubscriptionEvent, PROXY_WEBHOOK_EVENT_TYPES, type ProxyWebhookEventType, } from "./webhook-events.js";
12
+ export { PROXY_WEBHOOK_RETRY_AFTER_SECONDS, type ProxyWebhookClaimResult, type ProxyWebhookHandlerOptions, type ProxyWebhookOutcome, type ProxyWebhookPayloadInput, type ProxyWebhookProcessRecord, type ProxyWebhookProcessResult, type ProxyWebhookRequestInput, type ProxyWebhookStore, ProxyWebhooksResource, } from "./webhook-handler.js";
13
+ export { type ConstructProxyWebhookEventInput, type CreateWebhookEndpointInput, constructProxyWebhookEvent, PROXY_SIGNATURE_HEADER, ProxyWebhookEndpointsResource, type ProxyWebhookEvent, ProxyWebhookSignatureVerificationError, proxyCheckoutWebhookEndpointEndpoints, type RotateWebhookEndpointSecretInput, type UpdateWebhookEndpointInput, type VerifyProxyWebhookSignatureInput, verifyProxyWebhookSignature, type WebhookEndpoint, type WebhookEndpointSecretResult, } from "./webhooks.js";
package/dist/index.js CHANGED
@@ -1,6 +1,12 @@
1
+ export { parseProxyCart, } from "./cart.js";
1
2
  export { createProxyCheckoutServerClient, ProxyCheckoutServerClient, } from "./client.js";
2
- export { ProxyCheckoutApiError } from "./errors.js";
3
+ export { assertCartBuyerReference, assertSubscriptionMatchesSession, } from "./consistency.js";
4
+ export { ProxyCheckoutApiError, ProxyCheckoutValidationError, } from "./errors.js";
5
+ export { ProxyEventsResource, } from "./events.js";
6
+ export { AT_RISK_SUBSCRIPTION_STATUSES, ENDED_SUBSCRIPTION_STATUSES, isSessionProvisionable, isSessionTerminal, isSubscriptionEnded, isSubscriptionPaymentAtRisk, PROVISIONABLE_SESSION_STATUSES, parseOptionalProxyDate, requireProxyDate, subscriptionAccessEndsAt, subscriptionWillRenew, TERMINAL_SESSION_STATUSES, } from "./lifecycle.js";
3
7
  export { ProxySessionCartResource, ProxySessionsResource, proxyCheckoutServerEndpoints, } from "./sessions.js";
4
8
  export { ProxySubscriptionsResource, proxyCheckoutSubscriptionEndpoints, } from "./subscriptions.js";
5
9
  export { proxyCheckoutServerSdkName, proxyCheckoutServerSdkUserAgent, proxyCheckoutServerSdkVersion, } from "./version.js";
6
- export { constructProxyWebhookEvent, PROXY_SIGNATURE_HEADER, ProxyWebhookEndpointsResource, proxyCheckoutWebhookEndpointEndpoints, verifyProxyWebhookSignature, } from "./webhooks.js";
10
+ export { isProxyPaymentAttemptEvent, isProxySessionEvent, isProxySubscriptionEvent, PROXY_WEBHOOK_EVENT_TYPES, } from "./webhook-events.js";
11
+ export { PROXY_WEBHOOK_RETRY_AFTER_SECONDS, ProxyWebhooksResource, } from "./webhook-handler.js";
12
+ export { constructProxyWebhookEvent, PROXY_SIGNATURE_HEADER, ProxyWebhookEndpointsResource, ProxyWebhookSignatureVerificationError, proxyCheckoutWebhookEndpointEndpoints, verifyProxyWebhookSignature, } from "./webhooks.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * SDK-owned normalization of Proxy session and subscription lifecycle.
3
+ *
4
+ * These helpers turn raw Proxy state into the small set of derived facts a
5
+ * merchant entitlement layer actually needs (`willRenew`, `accessEndsAt`,
6
+ * terminal/provisionable predicates) so every customer does not re-encode
7
+ * Proxy lifecycle semantics by hand.
8
+ */
9
+ import type { MerchantProxySession } from "./sessions.js";
10
+ import type { MerchantProxySubscription } from "./subscriptions.js";
11
+ /** Session statuses that mean the payer has paid and entitlement may be granted. */
12
+ export declare const PROVISIONABLE_SESSION_STATUSES: Set<string>;
13
+ /** Session statuses that are terminal — no further lifecycle transitions occur. */
14
+ export declare const TERMINAL_SESSION_STATUSES: Set<string>;
15
+ /** Subscription statuses that mean the subscription is no longer active. */
16
+ export declare const ENDED_SUBSCRIPTION_STATUSES: Set<string>;
17
+ /** Subscription statuses that indicate a payment problem on an otherwise live subscription. */
18
+ export declare const AT_RISK_SUBSCRIPTION_STATUSES: Set<string>;
19
+ /** Parse a required ISO date from Proxy, throwing a structured error when missing/invalid. */
20
+ export declare function requireProxyDate(value: string | null | undefined, field: string): Date;
21
+ /** Parse an optional ISO date from Proxy, returning null when absent and throwing when invalid. */
22
+ export declare function parseOptionalProxyDate(value: string | null | undefined, field?: string): Date | null;
23
+ /** True once the payer has paid / the session is provisionable for entitlement. */
24
+ export declare function isSessionProvisionable(session: Pick<MerchantProxySession, "status">): boolean;
25
+ /** True once the session has reached a terminal state. */
26
+ export declare function isSessionTerminal(session: Pick<MerchantProxySession, "status">): boolean;
27
+ /**
28
+ * Whether the subscription is expected to renew at the end of the current period.
29
+ * A subscription will not renew once it is scheduled to cancel or has ended.
30
+ */
31
+ export declare function subscriptionWillRenew(subscription: Pick<MerchantProxySubscription, "cancelAtPeriodEnd" | "endedAt" | "status">): boolean;
32
+ /**
33
+ * The instant at which paid access should end given current subscription state.
34
+ * Prefers an explicit `endedAt` (hard stop) over the current period boundary.
35
+ * Returns null when neither is known.
36
+ */
37
+ export declare function subscriptionAccessEndsAt(subscription: Pick<MerchantProxySubscription, "currentPeriodEnd" | "endedAt">): Date | null;
38
+ /** True when the subscription has ended (cancelled / expired / explicitly ended). */
39
+ export declare function isSubscriptionEnded(subscription: Pick<MerchantProxySubscription, "endedAt" | "status">): boolean;
40
+ /** True when the subscription is live but has a payment problem (past_due / unpaid / failed). */
41
+ export declare function isSubscriptionPaymentAtRisk(subscription: Pick<MerchantProxySubscription, "latestPaymentStatus" | "status">): boolean;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * SDK-owned normalization of Proxy session and subscription lifecycle.
3
+ *
4
+ * These helpers turn raw Proxy state into the small set of derived facts a
5
+ * merchant entitlement layer actually needs (`willRenew`, `accessEndsAt`,
6
+ * terminal/provisionable predicates) so every customer does not re-encode
7
+ * Proxy lifecycle semantics by hand.
8
+ */
9
+ import { ProxyCheckoutValidationError } from "./errors.js";
10
+ /** Session statuses that mean the payer has paid and entitlement may be granted. */
11
+ export const PROVISIONABLE_SESSION_STATUSES = new Set(["paid", "provisionable"]);
12
+ /** Session statuses that are terminal — no further lifecycle transitions occur. */
13
+ export const TERMINAL_SESSION_STATUSES = new Set([
14
+ "cancelled",
15
+ "expired",
16
+ "failed",
17
+ "provisioned",
18
+ "provisioning_failed",
19
+ ]);
20
+ /** Subscription statuses that mean the subscription is no longer active. */
21
+ export const ENDED_SUBSCRIPTION_STATUSES = new Set([
22
+ "canceled",
23
+ "ended",
24
+ "incomplete_expired",
25
+ ]);
26
+ /** Subscription statuses that indicate a payment problem on an otherwise live subscription. */
27
+ export const AT_RISK_SUBSCRIPTION_STATUSES = new Set(["past_due", "unpaid"]);
28
+ /** Parse a required ISO date from Proxy, throwing a structured error when missing/invalid. */
29
+ export function requireProxyDate(value, field) {
30
+ const date = parseOptionalProxyDate(value, field);
31
+ if (date === null) {
32
+ throw new ProxyCheckoutValidationError(`Proxy date field ${field} is required.`, {
33
+ code: "invalid_date",
34
+ field,
35
+ });
36
+ }
37
+ return date;
38
+ }
39
+ /** Parse an optional ISO date from Proxy, returning null when absent and throwing when invalid. */
40
+ export function parseOptionalProxyDate(value, field = "date") {
41
+ if (value === null || value === undefined || value === "") {
42
+ return null;
43
+ }
44
+ const date = new Date(value);
45
+ if (Number.isNaN(date.getTime())) {
46
+ throw new ProxyCheckoutValidationError(`Proxy date field ${field} is not a valid date.`, {
47
+ code: "invalid_date",
48
+ field,
49
+ });
50
+ }
51
+ return date;
52
+ }
53
+ /** True once the payer has paid / the session is provisionable for entitlement. */
54
+ export function isSessionProvisionable(session) {
55
+ return PROVISIONABLE_SESSION_STATUSES.has(session.status);
56
+ }
57
+ /** True once the session has reached a terminal state. */
58
+ export function isSessionTerminal(session) {
59
+ return TERMINAL_SESSION_STATUSES.has(session.status);
60
+ }
61
+ /**
62
+ * Whether the subscription is expected to renew at the end of the current period.
63
+ * A subscription will not renew once it is scheduled to cancel or has ended.
64
+ */
65
+ export function subscriptionWillRenew(subscription) {
66
+ if (subscription.cancelAtPeriodEnd) {
67
+ return false;
68
+ }
69
+ if (subscription.endedAt !== null) {
70
+ return false;
71
+ }
72
+ return !ENDED_SUBSCRIPTION_STATUSES.has(subscription.status);
73
+ }
74
+ /**
75
+ * The instant at which paid access should end given current subscription state.
76
+ * Prefers an explicit `endedAt` (hard stop) over the current period boundary.
77
+ * Returns null when neither is known.
78
+ */
79
+ export function subscriptionAccessEndsAt(subscription) {
80
+ return (parseOptionalProxyDate(subscription.endedAt, "subscription.endedAt") ??
81
+ parseOptionalProxyDate(subscription.currentPeriodEnd, "subscription.currentPeriodEnd"));
82
+ }
83
+ /** True when the subscription has ended (cancelled / expired / explicitly ended). */
84
+ export function isSubscriptionEnded(subscription) {
85
+ return subscription.endedAt !== null || ENDED_SUBSCRIPTION_STATUSES.has(subscription.status);
86
+ }
87
+ /** True when the subscription is live but has a payment problem (past_due / unpaid / failed). */
88
+ export function isSubscriptionPaymentAtRisk(subscription) {
89
+ return (AT_RISK_SUBSCRIPTION_STATUSES.has(subscription.status) ||
90
+ subscription.latestPaymentStatus === "failed");
91
+ }
@@ -1,3 +1,4 @@
1
+ import { type ProxyCartParseOptions, type ProxyCartValidator } from "./cart.js";
1
2
  import type { ProxyCheckoutHttpClient } from "./http-client.js";
2
3
  import type { JsonObject, ProxyCheckoutServerRequestOptions } from "./types.js";
3
4
  export interface CreateProxySessionOptions extends ProxyCheckoutServerRequestOptions {
@@ -45,12 +46,18 @@ export interface MerchantProxySession {
45
46
  readonly email: string | null;
46
47
  readonly phone: string | null;
47
48
  } | null;
49
+ readonly providerCheckoutSessionId: string | null;
50
+ readonly providerCheckoutSessionPsp: string | null;
48
51
  readonly status: string;
49
52
  readonly updatedAt: string;
50
53
  }
51
54
  export interface PayerOpenedResult extends MerchantProxySession {
52
55
  readonly status: "payer_opened";
53
56
  }
57
+ /** A merchant session with its cart snapshot validated against a merchant schema. */
58
+ export interface TypedMerchantProxySession<TCart> extends MerchantProxySession {
59
+ readonly cart: TCart;
60
+ }
54
61
  export interface PayerHandoffResult {
55
62
  readonly status: "payer_handoff_pending" | "payer_opened";
56
63
  }
@@ -64,6 +71,16 @@ export interface ProxySessionCartResult {
64
71
  readonly cartSnapshot: JsonObject;
65
72
  readonly currency: string;
66
73
  }
74
+ export interface RecordProviderBindingInput extends ProxyCheckoutServerRequestOptions {
75
+ readonly providerCheckoutSessionId: string;
76
+ readonly psp: string;
77
+ }
78
+ export interface ProxySessionProviderBinding {
79
+ readonly providerCheckoutSessionId: string;
80
+ readonly proxySessionId: string;
81
+ readonly psp: string;
82
+ readonly updatedAt: string;
83
+ }
67
84
  export declare const proxyCheckoutServerEndpoints: readonly [{
68
85
  readonly method: "POST";
69
86
  readonly operation: "sessions.create";
@@ -88,6 +105,10 @@ export declare const proxyCheckoutServerEndpoints: readonly [{
88
105
  readonly method: "POST";
89
106
  readonly operation: "sessions.payerOpened";
90
107
  readonly path: "/proxy_sessions/:id/payer_opened";
108
+ }, {
109
+ readonly method: "POST";
110
+ readonly operation: "sessions.providerBindings.create";
111
+ readonly path: "/proxy_sessions/:id/provider_bindings";
91
112
  }];
92
113
  export declare class ProxySessionsResource {
93
114
  private readonly httpClient;
@@ -100,8 +121,26 @@ export declare class ProxySessionsResource {
100
121
  create(input: CreateProxySessionInput): Promise<ProxySession>;
101
122
  createHandoff(input: CreateProxySessionHandoffInput): Promise<ProxySessionHandoff>;
102
123
  retrieve(proxySessionId: string, options?: ProxyCheckoutServerRequestOptions): Promise<MerchantProxySession>;
124
+ /**
125
+ * Retrieve a session and validate its cart snapshot against a merchant schema,
126
+ * returning the session with a typed `cart`. Throws a structured error if the
127
+ * cart fails validation or its buyer reference disagrees with the session.
128
+ */
129
+ retrieveTyped<TCart>(proxySessionId: string, cartSchema: ProxyCartValidator<TCart>, options?: ProxyCheckoutServerRequestOptions & ProxyCartParseOptions<TCart>): Promise<TypedMerchantProxySession<TCart>>;
130
+ /**
131
+ * Validate the cart snapshot of an already-retrieved session against a merchant
132
+ * schema. When `getBuyerReference` is supplied, the cart buyer reference is
133
+ * asserted to match the session buyer reference.
134
+ */
135
+ parseCart<TCart>(session: MerchantProxySession, cartSchema: ProxyCartValidator<TCart>, options?: ProxyCartParseOptions<TCart>): TCart;
103
136
  payerHandoff(proxySessionId: string, options?: ProxyCheckoutServerRequestOptions): Promise<PayerHandoffResult>;
104
137
  payerOpened(proxySessionId: string, options?: ProxyCheckoutServerRequestOptions): Promise<PayerOpenedResult>;
138
+ /**
139
+ * Bind a provider (PSP) checkout object to a Proxy session so later cart sync
140
+ * can verify ownership. Idempotent for the same provider object; conflicts when
141
+ * the session is already bound to a different one.
142
+ */
143
+ recordProviderBinding(proxySessionId: string, input: RecordProviderBindingInput): Promise<ProxySessionProviderBinding>;
105
144
  }
106
145
  export declare class ProxySessionCartResource {
107
146
  private readonly httpClient;
package/dist/sessions.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { parseProxyCart } from "./cart.js";
2
+ import { assertCartBuyerReference } from "./consistency.js";
1
3
  import { requireInteger, requireJsonObject, requireNullableString, requireString, } from "./response-validators.js";
2
4
  export const proxyCheckoutServerEndpoints = [
3
5
  {
@@ -30,6 +32,11 @@ export const proxyCheckoutServerEndpoints = [
30
32
  operation: "sessions.payerOpened",
31
33
  path: "/proxy_sessions/:id/payer_opened",
32
34
  },
35
+ {
36
+ method: "POST",
37
+ operation: "sessions.providerBindings.create",
38
+ path: "/proxy_sessions/:id/provider_bindings",
39
+ },
33
40
  ];
34
41
  export class ProxySessionsResource {
35
42
  httpClient;
@@ -77,6 +84,27 @@ export class ProxySessionsResource {
77
84
  const response = await this.httpClient.request("GET", `/proxy_sessions/${encodeURIComponent(proxySessionId)}/merchant`, undefined, options);
78
85
  return toMerchantProxySession(response);
79
86
  }
87
+ /**
88
+ * Retrieve a session and validate its cart snapshot against a merchant schema,
89
+ * returning the session with a typed `cart`. Throws a structured error if the
90
+ * cart fails validation or its buyer reference disagrees with the session.
91
+ */
92
+ async retrieveTyped(proxySessionId, cartSchema, options = {}) {
93
+ const session = await this.retrieve(proxySessionId, { requestId: options.requestId });
94
+ return { ...session, cart: this.parseCart(session, cartSchema, options) };
95
+ }
96
+ /**
97
+ * Validate the cart snapshot of an already-retrieved session against a merchant
98
+ * schema. When `getBuyerReference` is supplied, the cart buyer reference is
99
+ * asserted to match the session buyer reference.
100
+ */
101
+ parseCart(session, cartSchema, options = {}) {
102
+ const cart = parseProxyCart(session.cartSnapshot, cartSchema);
103
+ if (options.getBuyerReference !== undefined) {
104
+ assertCartBuyerReference(options.getBuyerReference(cart), session);
105
+ }
106
+ return cart;
107
+ }
80
108
  async payerHandoff(proxySessionId, options = {}) {
81
109
  const response = await this.httpClient.request("POST", `/proxy_sessions/${encodeURIComponent(proxySessionId)}/payer_handoff`, {}, options);
82
110
  const body = requireJsonObject(response, "sessions.payerHandoff");
@@ -88,6 +116,24 @@ export class ProxySessionsResource {
88
116
  const response = await this.httpClient.request("POST", `/proxy_sessions/${encodeURIComponent(proxySessionId)}/payer_opened`, {}, options);
89
117
  return toPayerOpenedResult(response);
90
118
  }
119
+ /**
120
+ * Bind a provider (PSP) checkout object to a Proxy session so later cart sync
121
+ * can verify ownership. Idempotent for the same provider object; conflicts when
122
+ * the session is already bound to a different one.
123
+ */
124
+ async recordProviderBinding(proxySessionId, input) {
125
+ const response = await this.httpClient.request("POST", `/proxy_sessions/${encodeURIComponent(proxySessionId)}/provider_bindings`, {
126
+ provider_checkout_session_id: input.providerCheckoutSessionId,
127
+ psp: input.psp,
128
+ }, { requestId: input.requestId });
129
+ const body = requireJsonObject(response, "sessions.providerBindings.create");
130
+ return {
131
+ providerCheckoutSessionId: requireString(body.provider_checkout_session_id, "sessions.providerBindings.create.providerCheckoutSessionId"),
132
+ proxySessionId: requireString(body.proxy_session_id, "sessions.providerBindings.create.proxySessionId"),
133
+ psp: requireString(body.psp, "sessions.providerBindings.create.psp"),
134
+ updatedAt: requireString(body.updated_at, "sessions.providerBindings.create.updatedAt"),
135
+ };
136
+ }
91
137
  }
92
138
  export class ProxySessionCartResource {
93
139
  httpClient;
@@ -178,6 +224,8 @@ function toMerchantProxySession(response) {
178
224
  integrationMode: requireString(body.integration_mode, "sessions.retrieve.integrationMode"),
179
225
  metadata: requireJsonObject(body.metadata, "sessions.retrieve.metadata"),
180
226
  payerContact: requireNullablePayerContact(body.payer_contact),
227
+ providerCheckoutSessionId: requireNullableString(body.provider_checkout_session_id, "sessions.retrieve.providerCheckoutSessionId"),
228
+ providerCheckoutSessionPsp: requireNullableString(body.provider_checkout_session_psp, "sessions.retrieve.providerCheckoutSessionPsp"),
181
229
  status: requireString(body.status, "sessions.retrieve.status"),
182
230
  updatedAt: requireString(body.updated_at, "sessions.retrieve.updatedAt"),
183
231
  };
@@ -200,6 +248,9 @@ function toPayerOpenedResult(response) {
200
248
  integrationMode: requireString(body.integration_mode, "sessions.payerOpened.integrationMode"),
201
249
  metadata: requireJsonObject(body.metadata, "sessions.payerOpened.metadata"),
202
250
  payerContact: requireNullablePayerContact(body.payer_contact, "sessions.payerOpened"),
251
+ // No provider binding exists yet at payer-open; it is recorded after checkout creation.
252
+ providerCheckoutSessionId: null,
253
+ providerCheckoutSessionPsp: null,
203
254
  status,
204
255
  updatedAt: requireString(body.updated_at, "sessions.payerOpened.updatedAt"),
205
256
  };
@@ -1,4 +1,6 @@
1
+ import type { ProxyCartParseOptions, ProxyCartValidator } from "./cart.js";
1
2
  import type { ProxyCheckoutHttpClient } from "./http-client.js";
3
+ import type { MerchantProxySession, ProxySessionsResource, TypedMerchantProxySession } from "./sessions.js";
2
4
  import type { ProxyCheckoutServerRequestOptions } from "./types.js";
3
5
  export interface MerchantProxySubscription {
4
6
  readonly buyerReference: string;
@@ -25,8 +27,28 @@ export declare const proxyCheckoutSubscriptionEndpoints: readonly [{
25
27
  readonly operation: "subscriptions.retrieve";
26
28
  readonly path: "/subscriptions/:id";
27
29
  }];
30
+ /** A subscription paired with its verified original session (and optional typed cart). */
31
+ export interface ProxySubscriptionWithSession<TCart> {
32
+ readonly cart: TCart;
33
+ readonly session: TypedMerchantProxySession<TCart>;
34
+ readonly subscription: MerchantProxySubscription;
35
+ }
36
+ export interface ProxySubscriptionWithUntypedSession {
37
+ readonly session: MerchantProxySession;
38
+ readonly subscription: MerchantProxySubscription;
39
+ }
28
40
  export declare class ProxySubscriptionsResource {
29
41
  private readonly httpClient;
30
- constructor(httpClient: ProxyCheckoutHttpClient);
42
+ private readonly sessions;
43
+ constructor(httpClient: ProxyCheckoutHttpClient, sessions: ProxySessionsResource);
31
44
  retrieve(subscriptionId: string, options?: ProxyCheckoutServerRequestOptions): Promise<MerchantProxySubscription>;
45
+ /**
46
+ * Retrieve a subscription together with the original Proxy session it was
47
+ * created from, asserting buyer/session linkage. When `cartSchema` is given the
48
+ * session cart is validated and returned typed.
49
+ */
50
+ retrieveWithOriginalSession(subscriptionId: string, options?: ProxyCheckoutServerRequestOptions): Promise<ProxySubscriptionWithUntypedSession>;
51
+ retrieveWithOriginalSession<TCart>(subscriptionId: string, options: ProxyCheckoutServerRequestOptions & ProxyCartParseOptions<TCart> & {
52
+ cartSchema: ProxyCartValidator<TCart>;
53
+ }): Promise<ProxySubscriptionWithSession<TCart>>;
32
54
  }
@@ -1,3 +1,4 @@
1
+ import { assertSubscriptionMatchesSession } from "./consistency.js";
1
2
  import { requireBoolean, requireJsonObject, requireNullableInteger, requireNullableString, requireString, } from "./response-validators.js";
2
3
  export const proxyCheckoutSubscriptionEndpoints = [
3
4
  {
@@ -8,13 +9,26 @@ export const proxyCheckoutSubscriptionEndpoints = [
8
9
  ];
9
10
  export class ProxySubscriptionsResource {
10
11
  httpClient;
11
- constructor(httpClient) {
12
+ sessions;
13
+ constructor(httpClient, sessions) {
12
14
  this.httpClient = httpClient;
15
+ this.sessions = sessions;
13
16
  }
14
17
  async retrieve(subscriptionId, options = {}) {
15
18
  const response = await this.httpClient.request("GET", `/subscriptions/${encodeURIComponent(subscriptionId)}`, undefined, options);
16
19
  return toMerchantProxySubscription(response);
17
20
  }
21
+ async retrieveWithOriginalSession(subscriptionId, options = {}) {
22
+ const requestOptions = { requestId: options.requestId };
23
+ const subscription = await this.retrieve(subscriptionId, requestOptions);
24
+ const session = await this.sessions.retrieve(subscription.originalProxySessionId, requestOptions);
25
+ assertSubscriptionMatchesSession(subscription, session);
26
+ if (options.cartSchema === undefined) {
27
+ return { session, subscription };
28
+ }
29
+ const cart = this.sessions.parseCart(session, options.cartSchema, options);
30
+ return { cart, session: { ...session, cart }, subscription };
31
+ }
18
32
  }
19
33
  function toMerchantProxySubscription(response) {
20
34
  const body = requireJsonObject(response, "subscriptions.retrieve");
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export declare const proxyCheckoutServerSdkName = "@proxy-checkout/server-js";
2
- export declare const proxyCheckoutServerSdkVersion = "0.0.3";
3
- export declare const proxyCheckoutServerSdkUserAgent = "@proxy-checkout/server-js/0.0.3";
2
+ export declare const proxyCheckoutServerSdkVersion = "0.0.4-pr-76.13.1";
3
+ export declare const proxyCheckoutServerSdkUserAgent = "@proxy-checkout/server-js/0.0.4-pr-76.13.1";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export const proxyCheckoutServerSdkName = "@proxy-checkout/server-js";
2
- export const proxyCheckoutServerSdkVersion = "0.0.3";
2
+ export const proxyCheckoutServerSdkVersion = "0.0.4-pr-76.13.1";
3
3
  export const proxyCheckoutServerSdkUserAgent = `${proxyCheckoutServerSdkName}/${proxyCheckoutServerSdkVersion}`;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Canonical outbound Proxy webhook event types and predicates.
3
+ *
4
+ * These mirror the event types the Proxy API can deliver to merchant webhook
5
+ * endpoints. Use the predicates instead of comparing raw strings so customer
6
+ * integration code does not encode the event taxonomy by hand.
7
+ */
8
+ export declare const PROXY_WEBHOOK_EVENT_TYPES: {
9
+ readonly paymentAttemptCancelled: "payment_attempt.cancelled";
10
+ readonly paymentAttemptFailed: "payment_attempt.failed";
11
+ readonly paymentAttemptSucceeded: "payment_attempt.succeeded";
12
+ readonly sessionCancelled: "proxy_session.cancelled";
13
+ readonly sessionExpired: "proxy_session.expired";
14
+ readonly sessionFailed: "proxy_session.failed";
15
+ readonly sessionMerchantActionRequired: "proxy_session.merchant_action_required";
16
+ readonly sessionPaid: "proxy_session.paid";
17
+ readonly sessionProvisionable: "proxy_session.provisionable";
18
+ readonly subscriptionCancelled: "subscription.cancelled";
19
+ readonly subscriptionCancelScheduled: "subscription.cancel_scheduled";
20
+ readonly subscriptionPaymentFailed: "subscription.payment_failed";
21
+ readonly subscriptionRenewed: "subscription.renewed";
22
+ };
23
+ export type ProxyWebhookEventType = (typeof PROXY_WEBHOOK_EVENT_TYPES)[keyof typeof PROXY_WEBHOOK_EVENT_TYPES];
24
+ /** True for `proxy_session.*` events. */
25
+ export declare function isProxySessionEvent(eventType: string): boolean;
26
+ /** True for `subscription.*` events. */
27
+ export declare function isProxySubscriptionEvent(eventType: string): boolean;
28
+ /** True for `payment_attempt.*` events. */
29
+ export declare function isProxyPaymentAttemptEvent(eventType: string): boolean;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Canonical outbound Proxy webhook event types and predicates.
3
+ *
4
+ * These mirror the event types the Proxy API can deliver to merchant webhook
5
+ * endpoints. Use the predicates instead of comparing raw strings so customer
6
+ * integration code does not encode the event taxonomy by hand.
7
+ */
8
+ export const PROXY_WEBHOOK_EVENT_TYPES = {
9
+ paymentAttemptCancelled: "payment_attempt.cancelled",
10
+ paymentAttemptFailed: "payment_attempt.failed",
11
+ paymentAttemptSucceeded: "payment_attempt.succeeded",
12
+ sessionCancelled: "proxy_session.cancelled",
13
+ sessionExpired: "proxy_session.expired",
14
+ sessionFailed: "proxy_session.failed",
15
+ sessionMerchantActionRequired: "proxy_session.merchant_action_required",
16
+ sessionPaid: "proxy_session.paid",
17
+ sessionProvisionable: "proxy_session.provisionable",
18
+ subscriptionCancelled: "subscription.cancelled",
19
+ subscriptionCancelScheduled: "subscription.cancel_scheduled",
20
+ subscriptionPaymentFailed: "subscription.payment_failed",
21
+ subscriptionRenewed: "subscription.renewed",
22
+ };
23
+ const SESSION_EVENT_TYPES = new Set([
24
+ PROXY_WEBHOOK_EVENT_TYPES.sessionCancelled,
25
+ PROXY_WEBHOOK_EVENT_TYPES.sessionExpired,
26
+ PROXY_WEBHOOK_EVENT_TYPES.sessionFailed,
27
+ PROXY_WEBHOOK_EVENT_TYPES.sessionMerchantActionRequired,
28
+ PROXY_WEBHOOK_EVENT_TYPES.sessionPaid,
29
+ PROXY_WEBHOOK_EVENT_TYPES.sessionProvisionable,
30
+ ]);
31
+ const SUBSCRIPTION_EVENT_TYPES = new Set([
32
+ PROXY_WEBHOOK_EVENT_TYPES.subscriptionCancelScheduled,
33
+ PROXY_WEBHOOK_EVENT_TYPES.subscriptionCancelled,
34
+ PROXY_WEBHOOK_EVENT_TYPES.subscriptionPaymentFailed,
35
+ PROXY_WEBHOOK_EVENT_TYPES.subscriptionRenewed,
36
+ ]);
37
+ const PAYMENT_ATTEMPT_EVENT_TYPES = new Set([
38
+ PROXY_WEBHOOK_EVENT_TYPES.paymentAttemptCancelled,
39
+ PROXY_WEBHOOK_EVENT_TYPES.paymentAttemptFailed,
40
+ PROXY_WEBHOOK_EVENT_TYPES.paymentAttemptSucceeded,
41
+ ]);
42
+ /** True for `proxy_session.*` events. */
43
+ export function isProxySessionEvent(eventType) {
44
+ return SESSION_EVENT_TYPES.has(eventType);
45
+ }
46
+ /** True for `subscription.*` events. */
47
+ export function isProxySubscriptionEvent(eventType) {
48
+ return SUBSCRIPTION_EVENT_TYPES.has(eventType);
49
+ }
50
+ /** True for `payment_attempt.*` events. */
51
+ export function isProxyPaymentAttemptEvent(eventType) {
52
+ return PAYMENT_ATTEMPT_EVENT_TYPES.has(eventType);
53
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Server SDK webhook delivery handler.
3
+ *
4
+ * `handle` turns a verified Proxy webhook into a current-state lifecycle
5
+ * decision and runs the merchant's entitlement callback, with optional
6
+ * idempotency via a pluggable {@link ProxyWebhookStore}. It owns signature
7
+ * verification, duplicate/in-flight handling, retryable responses, and
8
+ * current-state retrieval so customer webhook routes stay tiny.
9
+ */
10
+ import type { ProxyEventsResource, ResolvedProxyEvent, ResolvedProxyEventKind } from "./events.js";
11
+ import { type ProxyWebhookEvent } from "./webhooks.js";
12
+ /** Seconds advertised in `Retry-After` when an event is already in flight. */
13
+ export declare const PROXY_WEBHOOK_RETRY_AFTER_SECONDS = 30;
14
+ export type ProxyWebhookClaimResult = "claimed" | "duplicate" | "processing";
15
+ export type ProxyWebhookOutcome = "duplicate" | "ignored" | "processed" | "processing";
16
+ /** Compact, lifecycle-free audit record handed to a {@link ProxyWebhookStore}. */
17
+ export interface ProxyWebhookProcessRecord {
18
+ readonly kind: ResolvedProxyEventKind;
19
+ readonly sessionId: string | null;
20
+ readonly subscriptionId: string | null;
21
+ }
22
+ /**
23
+ * Optional merchant-owned idempotency/audit store. The handler treats already
24
+ * processed events as success and concurrently in-flight events as retryable;
25
+ * it never classifies lifecycle itself.
26
+ */
27
+ export interface ProxyWebhookStore {
28
+ /** Atomically claim an event id. Return "duplicate" if already processed, "processing" if in flight. */
29
+ claim(event: ProxyWebhookEvent): Promise<ProxyWebhookClaimResult> | ProxyWebhookClaimResult;
30
+ /** Mark a claimed event failed so it can be retried/reclaimed. */
31
+ markFailed(eventId: string, error: unknown): Promise<void> | void;
32
+ /** Mark a claimed event processed (idempotency commit). */
33
+ markProcessed(eventId: string, record: ProxyWebhookProcessRecord): Promise<void> | void;
34
+ }
35
+ export interface ProxyWebhookPayloadInput {
36
+ readonly body: Buffer | string;
37
+ readonly signature?: string | null;
38
+ }
39
+ /**
40
+ * A web `Request` (Next.js route handlers, Hono on Node) or an explicit
41
+ * body/signature pair (Express/Node). Signature verification uses `node:crypto`,
42
+ * so this targets Node-compatible runtimes — not Cloudflare Workers.
43
+ */
44
+ export type ProxyWebhookRequestInput = ProxyWebhookPayloadInput | Request;
45
+ export interface ProxyWebhookHandlerOptions {
46
+ /** Merchant entitlement callback, invoked once per claimed event after current-state resolution. */
47
+ readonly onResolved: (resolved: ResolvedProxyEvent) => Promise<void> | void;
48
+ /** Optional request id forwarded to the current-state retrieval calls. */
49
+ readonly requestId?: string;
50
+ /** Endpoint signing secret. */
51
+ readonly secret: string;
52
+ /** Optional idempotency/audit store. Omit for at-least-once delivery with idempotent fulfillment. */
53
+ readonly store?: ProxyWebhookStore;
54
+ /** Signature timestamp tolerance in seconds (default 300). */
55
+ readonly toleranceSeconds?: number;
56
+ }
57
+ export interface ProxyWebhookProcessResult {
58
+ readonly event: ProxyWebhookEvent;
59
+ readonly outcome: ProxyWebhookOutcome;
60
+ readonly resolved: ResolvedProxyEvent | null;
61
+ readonly retryable: boolean;
62
+ readonly statusCode: number;
63
+ }
64
+ export declare class ProxyWebhooksResource {
65
+ private readonly events;
66
+ constructor(events: ProxyEventsResource);
67
+ /** Low-level: verify + construct the event without resolving lifecycle. */
68
+ constructEvent(input: {
69
+ body: Buffer | string;
70
+ header: string | null | undefined;
71
+ secret: string;
72
+ toleranceSeconds?: number;
73
+ }): ProxyWebhookEvent;
74
+ /**
75
+ * Framework-agnostic processing. Verifies the signature, applies the store,
76
+ * resolves current state, runs `onResolved`, and returns response metadata.
77
+ * Throws {@link ProxyWebhookSignatureVerificationError} on a bad signature so
78
+ * non-Response frameworks (Express) can map it to 400.
79
+ */
80
+ process(input: ProxyWebhookRequestInput, options: ProxyWebhookHandlerOptions): Promise<ProxyWebhookProcessResult>;
81
+ /**
82
+ * Turnkey handler for web `Request` frameworks. Returns a `Response` with the
83
+ * right status (200 success/duplicate, 409 + `Retry-After` in flight, 400 on
84
+ * bad signature). Re-throws merchant `onResolved` errors so the platform can
85
+ * surface a 500 and Proxy retries.
86
+ */
87
+ handle(input: ProxyWebhookRequestInput, options: ProxyWebhookHandlerOptions): Promise<Response>;
88
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Server SDK webhook delivery handler.
3
+ *
4
+ * `handle` turns a verified Proxy webhook into a current-state lifecycle
5
+ * decision and runs the merchant's entitlement callback, with optional
6
+ * idempotency via a pluggable {@link ProxyWebhookStore}. It owns signature
7
+ * verification, duplicate/in-flight handling, retryable responses, and
8
+ * current-state retrieval so customer webhook routes stay tiny.
9
+ */
10
+ import { constructProxyWebhookEvent, PROXY_SIGNATURE_HEADER, ProxyWebhookSignatureVerificationError, } from "./webhooks.js";
11
+ /** Seconds advertised in `Retry-After` when an event is already in flight. */
12
+ export const PROXY_WEBHOOK_RETRY_AFTER_SECONDS = 30;
13
+ export class ProxyWebhooksResource {
14
+ events;
15
+ constructor(events) {
16
+ this.events = events;
17
+ }
18
+ /** Low-level: verify + construct the event without resolving lifecycle. */
19
+ constructEvent(input) {
20
+ return constructProxyWebhookEvent(input);
21
+ }
22
+ /**
23
+ * Framework-agnostic processing. Verifies the signature, applies the store,
24
+ * resolves current state, runs `onResolved`, and returns response metadata.
25
+ * Throws {@link ProxyWebhookSignatureVerificationError} on a bad signature so
26
+ * non-Response frameworks (Express) can map it to 400.
27
+ */
28
+ async process(input, options) {
29
+ const { body, signature } = await readWebhookPayload(input);
30
+ const event = constructProxyWebhookEvent({
31
+ body,
32
+ header: signature,
33
+ secret: options.secret,
34
+ toleranceSeconds: options.toleranceSeconds,
35
+ });
36
+ const { store } = options;
37
+ if (store !== undefined) {
38
+ const claim = await store.claim(event);
39
+ if (claim === "duplicate") {
40
+ return {
41
+ event,
42
+ outcome: "duplicate",
43
+ resolved: null,
44
+ retryable: false,
45
+ statusCode: 200,
46
+ };
47
+ }
48
+ if (claim === "processing") {
49
+ return {
50
+ event,
51
+ outcome: "processing",
52
+ resolved: null,
53
+ retryable: true,
54
+ statusCode: 409,
55
+ };
56
+ }
57
+ }
58
+ let resolved;
59
+ try {
60
+ resolved = await this.events.resolveCurrentState(event, { requestId: options.requestId });
61
+ await options.onResolved(resolved);
62
+ }
63
+ catch (error) {
64
+ if (store !== undefined) {
65
+ try {
66
+ await store.markFailed(event.id, error);
67
+ }
68
+ catch {
69
+ // Never let a store failure shadow the original fulfillment error.
70
+ }
71
+ }
72
+ throw error;
73
+ }
74
+ if (store !== undefined) {
75
+ await store.markProcessed(event.id, toProcessRecord(resolved));
76
+ }
77
+ return {
78
+ event,
79
+ outcome: resolved.kind === "ignored" ? "ignored" : "processed",
80
+ resolved,
81
+ retryable: false,
82
+ statusCode: 200,
83
+ };
84
+ }
85
+ /**
86
+ * Turnkey handler for web `Request` frameworks. Returns a `Response` with the
87
+ * right status (200 success/duplicate, 409 + `Retry-After` in flight, 400 on
88
+ * bad signature). Re-throws merchant `onResolved` errors so the platform can
89
+ * surface a 500 and Proxy retries.
90
+ */
91
+ async handle(input, options) {
92
+ let result;
93
+ try {
94
+ result = await this.process(input, options);
95
+ }
96
+ catch (error) {
97
+ if (error instanceof ProxyWebhookSignatureVerificationError) {
98
+ return jsonResponse(400, {
99
+ error: { code: "invalid_signature", message: error.message },
100
+ });
101
+ }
102
+ throw error;
103
+ }
104
+ return toWebhookResponse(result);
105
+ }
106
+ }
107
+ function toWebhookResponse(result) {
108
+ const headers = { "content-type": "application/json" };
109
+ if (result.retryable) {
110
+ headers["retry-after"] = String(PROXY_WEBHOOK_RETRY_AFTER_SECONDS);
111
+ }
112
+ return new Response(JSON.stringify({ ok: !result.retryable, outcome: result.outcome }), {
113
+ headers,
114
+ status: result.statusCode,
115
+ });
116
+ }
117
+ function jsonResponse(status, body) {
118
+ return new Response(JSON.stringify(body), {
119
+ headers: { "content-type": "application/json" },
120
+ status,
121
+ });
122
+ }
123
+ function toProcessRecord(resolved) {
124
+ switch (resolved.kind) {
125
+ case "ignored":
126
+ return {
127
+ kind: resolved.kind,
128
+ sessionId: resolved.sessionId,
129
+ subscriptionId: resolved.subscriptionId,
130
+ };
131
+ case "terminal_session":
132
+ return { kind: resolved.kind, sessionId: resolved.session.id, subscriptionId: null };
133
+ case "initial_provision":
134
+ return {
135
+ kind: resolved.kind,
136
+ sessionId: resolved.session.id,
137
+ subscriptionId: resolved.subscription?.id ?? null,
138
+ };
139
+ default:
140
+ return {
141
+ kind: resolved.kind,
142
+ sessionId: resolved.session.id,
143
+ subscriptionId: resolved.subscription.id,
144
+ };
145
+ }
146
+ }
147
+ async function readWebhookPayload(input) {
148
+ if (isFetchRequest(input)) {
149
+ return {
150
+ body: await input.text(),
151
+ signature: input.headers.get(PROXY_SIGNATURE_HEADER),
152
+ };
153
+ }
154
+ const body = typeof input.body === "string" ? input.body : input.body.toString("utf8");
155
+ return { body, signature: input.signature ?? null };
156
+ }
157
+ function isFetchRequest(input) {
158
+ return typeof input.text === "function";
159
+ }
@@ -41,6 +41,10 @@ export interface ProxyWebhookEvent {
41
41
  readonly type: string;
42
42
  }
43
43
  export declare const PROXY_SIGNATURE_HEADER = "proxy-signature";
44
+ /** Raised when a webhook payload fails signature verification. */
45
+ export declare class ProxyWebhookSignatureVerificationError extends Error {
46
+ readonly name = "ProxyWebhookSignatureVerificationError";
47
+ }
44
48
  export declare const proxyCheckoutWebhookEndpointEndpoints: readonly [{
45
49
  readonly method: "GET";
46
50
  readonly operation: "webhookEndpoints.list";
package/dist/webhooks.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { requireStringArrayOrNull as readStringArrayOrNull, requireNullableString as readStringOrNull, requireJsonObject, requireString, } from "./response-validators.js";
3
3
  export const PROXY_SIGNATURE_HEADER = "proxy-signature";
4
+ /** Raised when a webhook payload fails signature verification. */
5
+ export class ProxyWebhookSignatureVerificationError extends Error {
6
+ name = "ProxyWebhookSignatureVerificationError";
7
+ }
4
8
  export const proxyCheckoutWebhookEndpointEndpoints = [
5
9
  {
6
10
  method: "GET",
@@ -90,7 +94,7 @@ export function verifyProxyWebhookSignature(input) {
90
94
  }
91
95
  export function constructProxyWebhookEvent(input) {
92
96
  if (!verifyProxyWebhookSignature(input)) {
93
- throw new Error("Proxy webhook signature verification failed.");
97
+ throw new ProxyWebhookSignatureVerificationError("Proxy webhook signature verification failed.");
94
98
  }
95
99
  const body = typeof input.body === "string" ? input.body : input.body.toString("utf8");
96
100
  let parsed;
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "sideEffects": false,
8
8
  "type": "module",
9
9
  "types": "./dist/index.d.ts",
10
- "version": "0.0.3",
10
+ "version": "0.0.4-pr-76.13.1",
11
11
  "devDependencies": {
12
12
  "@types/node": "^24.12.4",
13
13
  "@vitest/coverage-v8": "4.1.7",