@proxy-checkout/server-js 0.0.1 → 0.0.3-pr-76.12.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.
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import type { JsonObject } from "./types.js";
2
+ export declare function requireJsonObject(value: unknown, operation: string): JsonObject;
3
+ export declare function requireString(value: unknown, field: string): string;
4
+ export declare function requireNullableString(value: unknown, field: string): string | null;
5
+ export declare function requireInteger(value: unknown, field: string): number;
6
+ export declare function requireNullableInteger(value: unknown, field: string): number | null;
7
+ export declare function requireBoolean(value: unknown, field: string): boolean;
8
+ export declare function requireStringArrayOrNull(value: unknown, field: string): string[] | null;
@@ -0,0 +1,51 @@
1
+ export function requireJsonObject(value, operation) {
2
+ if (!isRecord(value)) {
3
+ throw new Error(`Proxy API response for ${operation} must be a JSON object.`);
4
+ }
5
+ return value;
6
+ }
7
+ export function requireString(value, field) {
8
+ if (typeof value !== "string") {
9
+ throw new Error(`Proxy API response field ${field} must be a string.`);
10
+ }
11
+ return value;
12
+ }
13
+ export function requireNullableString(value, field) {
14
+ if (value === null) {
15
+ return null;
16
+ }
17
+ return requireString(value, field);
18
+ }
19
+ export function requireInteger(value, field) {
20
+ if (typeof value !== "number" || !Number.isInteger(value)) {
21
+ throw new Error(`Proxy API response field ${field} must be an integer.`);
22
+ }
23
+ return value;
24
+ }
25
+ export function requireNullableInteger(value, field) {
26
+ if (value === null) {
27
+ return null;
28
+ }
29
+ if (typeof value !== "number" || !Number.isInteger(value)) {
30
+ throw new Error(`Proxy API response field ${field} must be an integer or null.`);
31
+ }
32
+ return value;
33
+ }
34
+ export function requireBoolean(value, field) {
35
+ if (typeof value !== "boolean") {
36
+ throw new Error(`Proxy API response field ${field} must be a boolean.`);
37
+ }
38
+ return value;
39
+ }
40
+ export function requireStringArrayOrNull(value, field) {
41
+ if (value === null) {
42
+ return null;
43
+ }
44
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
45
+ throw new Error(`Proxy API response field ${field} must be an array or null.`);
46
+ }
47
+ return value;
48
+ }
49
+ function isRecord(value) {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value);
51
+ }
@@ -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 {
@@ -16,16 +17,46 @@ export interface CreateProxySessionInput extends CreateProxySessionOptions {
16
17
  readonly phone?: string;
17
18
  };
18
19
  }
20
+ export interface CreateProxySessionHandoffInput extends CreateProxySessionInput {
21
+ readonly payHost?: string;
22
+ readonly publishableKey?: string;
23
+ }
19
24
  export interface ProxySession {
20
25
  readonly expiresAt: string;
21
26
  readonly id: string;
22
27
  readonly integrationMode: string;
23
28
  readonly status: string;
24
29
  }
25
- export interface PayerOpenedResult {
30
+ export interface ProxySessionHandoff extends ProxySession {
31
+ readonly handoffUrl: string;
32
+ readonly status: "payer_handoff_pending" | "payer_opened";
33
+ }
34
+ export interface MerchantProxySession {
26
35
  readonly amountMinor: number;
27
- readonly cartDetails: JsonObject;
36
+ readonly buyerReference: string;
37
+ readonly cartSnapshot: JsonObject;
38
+ readonly createdAt: string;
28
39
  readonly currency: string;
40
+ readonly expiresAt: string;
41
+ readonly id: string;
42
+ readonly idempotencyKey: string | null;
43
+ readonly integrationMode: string;
44
+ readonly metadata: JsonObject;
45
+ readonly payerContact: {
46
+ readonly email: string | null;
47
+ readonly phone: string | null;
48
+ } | null;
49
+ readonly providerCheckoutSessionId: string | null;
50
+ readonly providerCheckoutSessionPsp: string | null;
51
+ readonly status: string;
52
+ readonly updatedAt: string;
53
+ }
54
+ export interface PayerOpenedResult extends MerchantProxySession {
55
+ readonly status: "payer_opened";
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;
29
60
  }
30
61
  export interface PayerHandoffResult {
31
62
  readonly status: "payer_handoff_pending" | "payer_opened";
@@ -40,10 +71,28 @@ export interface ProxySessionCartResult {
40
71
  readonly cartSnapshot: JsonObject;
41
72
  readonly currency: string;
42
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
+ }
43
84
  export declare const proxyCheckoutServerEndpoints: readonly [{
44
85
  readonly method: "POST";
45
86
  readonly operation: "sessions.create";
46
87
  readonly path: "/proxy_sessions";
88
+ }, {
89
+ readonly method: "POST";
90
+ readonly operation: "sessions.createHandoff";
91
+ readonly path: "/proxy_sessions/handoff";
92
+ }, {
93
+ readonly method: "GET";
94
+ readonly operation: "sessions.retrieve";
95
+ readonly path: "/proxy_sessions/:id/merchant";
47
96
  }, {
48
97
  readonly method: "PUT";
49
98
  readonly operation: "sessions.cart.set";
@@ -56,14 +105,42 @@ export declare const proxyCheckoutServerEndpoints: readonly [{
56
105
  readonly method: "POST";
57
106
  readonly operation: "sessions.payerOpened";
58
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";
59
112
  }];
60
113
  export declare class ProxySessionsResource {
61
114
  private readonly httpClient;
115
+ private readonly options;
62
116
  readonly cart: ProxySessionCartResource;
63
- constructor(httpClient: ProxyCheckoutHttpClient);
117
+ constructor(httpClient: ProxyCheckoutHttpClient, options?: {
118
+ payHost?: string;
119
+ publishableKey?: string;
120
+ });
64
121
  create(input: CreateProxySessionInput): Promise<ProxySession>;
122
+ createHandoff(input: CreateProxySessionHandoffInput): Promise<ProxySessionHandoff>;
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;
65
136
  payerHandoff(proxySessionId: string, options?: ProxyCheckoutServerRequestOptions): Promise<PayerHandoffResult>;
66
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>;
67
144
  }
68
145
  export declare class ProxySessionCartResource {
69
146
  private readonly httpClient;
package/dist/sessions.js CHANGED
@@ -1,9 +1,22 @@
1
+ import { parseProxyCart } from "./cart.js";
2
+ import { assertCartBuyerReference } from "./consistency.js";
3
+ import { requireInteger, requireJsonObject, requireNullableString, requireString, } from "./response-validators.js";
1
4
  export const proxyCheckoutServerEndpoints = [
2
5
  {
3
6
  method: "POST",
4
7
  operation: "sessions.create",
5
8
  path: "/proxy_sessions",
6
9
  },
10
+ {
11
+ method: "POST",
12
+ operation: "sessions.createHandoff",
13
+ path: "/proxy_sessions/handoff",
14
+ },
15
+ {
16
+ method: "GET",
17
+ operation: "sessions.retrieve",
18
+ path: "/proxy_sessions/:id/merchant",
19
+ },
7
20
  {
8
21
  method: "PUT",
9
22
  operation: "sessions.cart.set",
@@ -19,12 +32,19 @@ export const proxyCheckoutServerEndpoints = [
19
32
  operation: "sessions.payerOpened",
20
33
  path: "/proxy_sessions/:id/payer_opened",
21
34
  },
35
+ {
36
+ method: "POST",
37
+ operation: "sessions.providerBindings.create",
38
+ path: "/proxy_sessions/:id/provider_bindings",
39
+ },
22
40
  ];
23
41
  export class ProxySessionsResource {
24
42
  httpClient;
43
+ options;
25
44
  cart;
26
- constructor(httpClient) {
45
+ constructor(httpClient, options = {}) {
27
46
  this.httpClient = httpClient;
47
+ this.options = options;
28
48
  this.cart = new ProxySessionCartResource(httpClient);
29
49
  }
30
50
  async create(input) {
@@ -34,6 +54,57 @@ export class ProxySessionsResource {
34
54
  });
35
55
  return toProxySession(response);
36
56
  }
57
+ async createHandoff(input) {
58
+ const publishableKey = input.publishableKey ?? this.options.publishableKey;
59
+ if (!publishableKey) {
60
+ throw new Error("Proxy Checkout sessions.createHandoff requires a publishableKey option or input field.");
61
+ }
62
+ const payHost = input.payHost ?? this.options.payHost;
63
+ if (!payHost && this.httpClient.apiHost !== "https://api.proxycheckout.com") {
64
+ throw new Error("Proxy Checkout sessions.createHandoff requires a payHost option or input field when apiHost is overridden.");
65
+ }
66
+ const response = await this.httpClient.request("POST", "/proxy_sessions/handoff", {
67
+ ...toCreateProxySessionBody(input),
68
+ publishable_key: publishableKey,
69
+ }, {
70
+ idempotencyKey: input.idempotencyKey,
71
+ requestId: input.requestId,
72
+ });
73
+ const session = toProxySessionHandoff(response);
74
+ return {
75
+ ...session,
76
+ handoffUrl: buildHandoffUrl({
77
+ payHost,
78
+ proxySessionId: session.id,
79
+ publishableKey,
80
+ }),
81
+ };
82
+ }
83
+ async retrieve(proxySessionId, options = {}) {
84
+ const response = await this.httpClient.request("GET", `/proxy_sessions/${encodeURIComponent(proxySessionId)}/merchant`, undefined, options);
85
+ return toMerchantProxySession(response);
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
+ }
37
108
  async payerHandoff(proxySessionId, options = {}) {
38
109
  const response = await this.httpClient.request("POST", `/proxy_sessions/${encodeURIComponent(proxySessionId)}/payer_handoff`, {}, options);
39
110
  const body = requireJsonObject(response, "sessions.payerHandoff");
@@ -43,11 +114,24 @@ export class ProxySessionsResource {
43
114
  }
44
115
  async payerOpened(proxySessionId, options = {}) {
45
116
  const response = await this.httpClient.request("POST", `/proxy_sessions/${encodeURIComponent(proxySessionId)}/payer_opened`, {}, options);
46
- const body = requireJsonObject(response, "sessions.payerOpened");
117
+ return toPayerOpenedResult(response);
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");
47
130
  return {
48
- amountMinor: requireInteger(body.amount_minor, "sessions.payerOpened.amountMinor"),
49
- cartDetails: requireJsonObject(body.cart_details, "sessions.payerOpened"),
50
- currency: requireString(body.currency, "sessions.payerOpened.currency"),
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"),
51
135
  };
52
136
  }
53
137
  }
@@ -114,23 +198,72 @@ function toProxySession(response) {
114
198
  status,
115
199
  };
116
200
  }
117
- function requireJsonObject(value, operation) {
118
- if (!isRecord(value)) {
119
- throw new Error(`Proxy API response for ${operation} must be a JSON object.`);
120
- }
121
- return value;
201
+ function toProxySessionHandoff(response) {
202
+ const body = requireJsonObject(response, "sessions.createHandoff");
203
+ const status = requirePayerHandoffStatus(body.status);
204
+ const integrationMode = requireString(body.integration_mode, "sessions.createHandoff.integrationMode");
205
+ return {
206
+ expiresAt: requireString(body.expires_at, "sessions.createHandoff.expiresAt"),
207
+ handoffUrl: "",
208
+ id: requireString(body.id, "sessions.createHandoff.id"),
209
+ integrationMode,
210
+ status,
211
+ };
122
212
  }
123
- function requireString(value, field) {
124
- if (typeof value !== "string") {
125
- throw new Error(`Proxy API response field ${field} must be a string.`);
213
+ function toMerchantProxySession(response) {
214
+ const body = requireJsonObject(response, "sessions.retrieve");
215
+ return {
216
+ amountMinor: requireInteger(body.amount_minor, "sessions.retrieve.amountMinor"),
217
+ buyerReference: requireString(body.buyer_reference, "sessions.retrieve.buyerReference"),
218
+ cartSnapshot: requireJsonObject(body.cart_snapshot, "sessions.retrieve.cartSnapshot"),
219
+ createdAt: requireString(body.created_at, "sessions.retrieve.createdAt"),
220
+ currency: requireString(body.currency, "sessions.retrieve.currency"),
221
+ expiresAt: requireString(body.expires_at, "sessions.retrieve.expiresAt"),
222
+ id: requireString(body.id, "sessions.retrieve.id"),
223
+ idempotencyKey: requireNullableString(body.idempotency_key, "sessions.retrieve.idempotencyKey"),
224
+ integrationMode: requireString(body.integration_mode, "sessions.retrieve.integrationMode"),
225
+ metadata: requireJsonObject(body.metadata, "sessions.retrieve.metadata"),
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"),
229
+ status: requireString(body.status, "sessions.retrieve.status"),
230
+ updatedAt: requireString(body.updated_at, "sessions.retrieve.updatedAt"),
231
+ };
232
+ }
233
+ function toPayerOpenedResult(response) {
234
+ const body = requireJsonObject(response, "sessions.payerOpened");
235
+ const status = requireString(body.status, "sessions.payerOpened.status");
236
+ if (status !== "payer_opened") {
237
+ throw new Error("Proxy API response field sessions.payerOpened.status is unsupported.");
126
238
  }
127
- return value;
239
+ return {
240
+ amountMinor: requireInteger(body.amount_minor, "sessions.payerOpened.amountMinor"),
241
+ buyerReference: requireString(body.buyer_reference, "sessions.payerOpened.buyerReference"),
242
+ cartSnapshot: requireJsonObject(body.cart_snapshot, "sessions.payerOpened.cartSnapshot"),
243
+ createdAt: requireString(body.created_at, "sessions.payerOpened.createdAt"),
244
+ currency: requireString(body.currency, "sessions.payerOpened.currency"),
245
+ expiresAt: requireString(body.expires_at, "sessions.payerOpened.expiresAt"),
246
+ id: requireString(body.id, "sessions.payerOpened.id"),
247
+ idempotencyKey: requireNullableString(body.idempotency_key, "sessions.payerOpened.idempotencyKey"),
248
+ integrationMode: requireString(body.integration_mode, "sessions.payerOpened.integrationMode"),
249
+ metadata: requireJsonObject(body.metadata, "sessions.payerOpened.metadata"),
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,
254
+ status,
255
+ updatedAt: requireString(body.updated_at, "sessions.payerOpened.updatedAt"),
256
+ };
128
257
  }
129
- function requireInteger(value, field) {
130
- if (typeof value !== "number" || !Number.isInteger(value)) {
131
- throw new Error(`Proxy API response field ${field} must be an integer.`);
258
+ function requireNullablePayerContact(value, operation = "sessions.retrieve") {
259
+ if (value === null) {
260
+ return null;
132
261
  }
133
- return value;
262
+ const contact = requireJsonObject(value, `${operation}.payerContact`);
263
+ return {
264
+ email: requireNullableString(contact.email, `${operation}.payerContact.email`),
265
+ phone: requireNullableString(contact.phone, `${operation}.payerContact.phone`),
266
+ };
134
267
  }
135
268
  function requirePayerHandoffStatus(value) {
136
269
  const status = requireString(value, "sessions.payerHandoff.status");
@@ -139,6 +272,24 @@ function requirePayerHandoffStatus(value) {
139
272
  }
140
273
  throw new Error("Proxy API response field sessions.payerHandoff.status is unsupported.");
141
274
  }
142
- function isRecord(value) {
143
- return typeof value === "object" && value !== null && !Array.isArray(value);
275
+ function buildHandoffUrl({ payHost, proxySessionId, publishableKey, }) {
276
+ const url = new URL(`${normalizePayHost(payHost)}/s/${encodeURIComponent(proxySessionId)}`);
277
+ url.searchParams.set("pk", publishableKey);
278
+ return url.toString();
279
+ }
280
+ function normalizePayHost(payHost) {
281
+ if (payHost === undefined) {
282
+ return "https://pay.proxycheckout.com";
283
+ }
284
+ let url;
285
+ try {
286
+ url = new URL(payHost);
287
+ }
288
+ catch {
289
+ throw new Error(`Invalid payHost URL: ${payHost}. Make sure it is an absolute HTTP or HTTPS URL.`);
290
+ }
291
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
292
+ throw new Error(`Invalid payHost URL: ${payHost}. Make sure it is an absolute HTTP or HTTPS URL.`);
293
+ }
294
+ return `${url.origin}${url.pathname}`.replace(/\/$/, "");
144
295
  }
@@ -0,0 +1,54 @@
1
+ import type { ProxyCartParseOptions, ProxyCartValidator } from "./cart.js";
2
+ import type { ProxyCheckoutHttpClient } from "./http-client.js";
3
+ import type { MerchantProxySession, ProxySessionsResource, TypedMerchantProxySession } from "./sessions.js";
4
+ import type { ProxyCheckoutServerRequestOptions } from "./types.js";
5
+ export interface MerchantProxySubscription {
6
+ readonly buyerReference: string;
7
+ readonly cancelAtPeriodEnd: boolean;
8
+ readonly createdAt: string;
9
+ readonly currentPeriodEnd: string | null;
10
+ readonly currentPeriodStart: string | null;
11
+ readonly endedAt: string | null;
12
+ readonly id: string;
13
+ readonly latestInvoiceAmountMinor: number | null;
14
+ readonly latestInvoiceCurrency: string | null;
15
+ readonly latestInvoiceId: string | null;
16
+ readonly latestPaymentStatus: string | null;
17
+ readonly originalProxySessionId: string;
18
+ readonly providerCheckoutSessionId: string | null;
19
+ readonly providerSubscriptionId: string;
20
+ readonly psp: string;
21
+ readonly status: string;
22
+ readonly trialEnd: string | null;
23
+ readonly updatedAt: string;
24
+ }
25
+ export declare const proxyCheckoutSubscriptionEndpoints: readonly [{
26
+ readonly method: "GET";
27
+ readonly operation: "subscriptions.retrieve";
28
+ readonly path: "/subscriptions/:id";
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
+ }
40
+ export declare class ProxySubscriptionsResource {
41
+ private readonly httpClient;
42
+ private readonly sessions;
43
+ constructor(httpClient: ProxyCheckoutHttpClient, sessions: ProxySessionsResource);
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>>;
54
+ }
@@ -0,0 +1,55 @@
1
+ import { assertSubscriptionMatchesSession } from "./consistency.js";
2
+ import { requireBoolean, requireJsonObject, requireNullableInteger, requireNullableString, requireString, } from "./response-validators.js";
3
+ export const proxyCheckoutSubscriptionEndpoints = [
4
+ {
5
+ method: "GET",
6
+ operation: "subscriptions.retrieve",
7
+ path: "/subscriptions/:id",
8
+ },
9
+ ];
10
+ export class ProxySubscriptionsResource {
11
+ httpClient;
12
+ sessions;
13
+ constructor(httpClient, sessions) {
14
+ this.httpClient = httpClient;
15
+ this.sessions = sessions;
16
+ }
17
+ async retrieve(subscriptionId, options = {}) {
18
+ const response = await this.httpClient.request("GET", `/subscriptions/${encodeURIComponent(subscriptionId)}`, undefined, options);
19
+ return toMerchantProxySubscription(response);
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
+ }
32
+ }
33
+ function toMerchantProxySubscription(response) {
34
+ const body = requireJsonObject(response, "subscriptions.retrieve");
35
+ return {
36
+ buyerReference: requireString(body.buyer_reference, "subscriptions.retrieve.buyerReference"),
37
+ cancelAtPeriodEnd: requireBoolean(body.cancel_at_period_end, "subscriptions.retrieve.cancelAtPeriodEnd"),
38
+ createdAt: requireString(body.created_at, "subscriptions.retrieve.createdAt"),
39
+ currentPeriodEnd: requireNullableString(body.current_period_end, "subscriptions.retrieve.currentPeriodEnd"),
40
+ currentPeriodStart: requireNullableString(body.current_period_start, "subscriptions.retrieve.currentPeriodStart"),
41
+ endedAt: requireNullableString(body.ended_at, "subscriptions.retrieve.endedAt"),
42
+ id: requireString(body.id, "subscriptions.retrieve.id"),
43
+ latestInvoiceAmountMinor: requireNullableInteger(body.latest_invoice_amount_minor, "subscriptions.retrieve.latestInvoiceAmountMinor"),
44
+ latestInvoiceCurrency: requireNullableString(body.latest_invoice_currency, "subscriptions.retrieve.latestInvoiceCurrency"),
45
+ latestInvoiceId: requireNullableString(body.latest_invoice_id, "subscriptions.retrieve.latestInvoiceId"),
46
+ latestPaymentStatus: requireNullableString(body.latest_payment_status, "subscriptions.retrieve.latestPaymentStatus"),
47
+ originalProxySessionId: requireString(body.original_proxy_session_id, "subscriptions.retrieve.originalProxySessionId"),
48
+ providerCheckoutSessionId: requireNullableString(body.provider_checkout_session_id, "subscriptions.retrieve.providerCheckoutSessionId"),
49
+ providerSubscriptionId: requireString(body.provider_subscription_id, "subscriptions.retrieve.providerSubscriptionId"),
50
+ psp: requireString(body.psp, "subscriptions.retrieve.psp"),
51
+ status: requireString(body.status, "subscriptions.retrieve.status"),
52
+ trialEnd: requireNullableString(body.trial_end, "subscriptions.retrieve.trialEnd"),
53
+ updatedAt: requireString(body.updated_at, "subscriptions.retrieve.updatedAt"),
54
+ };
55
+ }
package/dist/types.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface ProxyCheckoutServerClientOptions {
8
8
  readonly apiHost?: string;
9
9
  readonly apiKey: string;
10
10
  readonly fetch?: ProxyCheckoutFetch;
11
+ readonly payHost?: string;
12
+ readonly publishableKey?: string;
11
13
  }
12
14
  export interface ProxyCheckoutServerRequestOptions {
13
15
  readonly requestId?: string;
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.1";
3
- export declare const proxyCheckoutServerSdkUserAgent = "@proxy-checkout/server-js/0.0.1";
2
+ export declare const proxyCheckoutServerSdkVersion = "0.0.3-pr-76.12.1";
3
+ export declare const proxyCheckoutServerSdkUserAgent = "@proxy-checkout/server-js/0.0.3-pr-76.12.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.1";
2
+ export const proxyCheckoutServerSdkVersion = "0.0.3-pr-76.12.1";
3
3
  export const proxyCheckoutServerSdkUserAgent = `${proxyCheckoutServerSdkName}/${proxyCheckoutServerSdkVersion}`;