@replohq/sdk 2.18.2 → 2.18.3

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.
@@ -1,5 +1,6 @@
1
1
  "use server";
2
2
  import { cookies } from "next/headers";
3
+ import { readBuyerIp } from "../lib/buyer-ip";
3
4
  import { getCheckoutProvider } from "../lib/integration-utils";
4
5
  import { REPLO_ATTRIBUTION_PROPERTY } from "./cart-types";
5
6
  import {
@@ -23,7 +24,8 @@ async function reconcileExistingCart(cart, country) {
23
24
  if (country && reconciledCart.buyerIdentity?.country !== country) {
24
25
  return await cartGateway.updateCartBuyerIdentity({
25
26
  cartId: reconciledCart.id,
26
- buyerIdentity: { country }
27
+ buyerIdentity: { country },
28
+ buyerIp: await readBuyerIp()
27
29
  });
28
30
  }
29
31
  return reconciledCart;
@@ -51,9 +53,10 @@ async function getCartAction() {
51
53
  const cartGateway = await getCartGateway();
52
54
  const sharedToken = checkoutProvider === "shopify" ? cookieStore.get(SHARED_SHOPIFY_CART_COOKIE_NAME)?.value : void 0;
53
55
  if (sharedToken) {
54
- const sharedCart = await cartGateway.getCart(
55
- `${SHOPIFY_CART_ID_PREFIX}${sharedToken}`
56
- );
56
+ const sharedCart = await cartGateway.getCart({
57
+ cartId: `${SHOPIFY_CART_ID_PREFIX}${sharedToken}`,
58
+ buyerIp: await readBuyerIp()
59
+ });
57
60
  if (sharedCart) {
58
61
  return sharedCart;
59
62
  }
@@ -62,7 +65,10 @@ async function getCartAction() {
62
65
  if (!existingCartId) {
63
66
  return null;
64
67
  }
65
- const cart = await cartGateway.getCart(existingCartId);
68
+ const cart = await cartGateway.getCart({
69
+ cartId: existingCartId,
70
+ buyerIp: await readBuyerIp()
71
+ });
66
72
  if (!cart) {
67
73
  try {
68
74
  cookieStore.set(SHOPIFY_CART_COOKIE_NAME, "", {
@@ -92,7 +98,8 @@ async function createCartAction(options) {
92
98
  lines,
93
99
  discountCodes,
94
100
  attributes,
95
- buyerIdentity: country ? { country } : void 0
101
+ buyerIdentity: country ? { country } : void 0,
102
+ buyerIp: await readBuyerIp()
96
103
  });
97
104
  if (cart && !skipStoringCart) {
98
105
  try {
@@ -117,7 +124,8 @@ async function createCartAction(options) {
117
124
  }
118
125
  async function addToCartAction(lines, cartId) {
119
126
  const cartGateway = await getCartGateway();
120
- const cart = await cartGateway.addToCart({ lines, cartId });
127
+ const buyerIp = await readBuyerIp();
128
+ const cart = await cartGateway.addToCart({ lines, cartId, buyerIp });
121
129
  if (!cart || await getCheckoutProvider() !== "shopify" || cart.attributes?.some(
122
130
  (attribute) => attribute.key === REPLO_ATTRIBUTION_PROPERTY.key
123
131
  )) {
@@ -125,29 +133,50 @@ async function addToCartAction(lines, cartId) {
125
133
  }
126
134
  const attributedCart = await cartGateway.replaceCartAttributes({
127
135
  cartId: cart.id,
128
- attributes: [...cart.attributes ?? [], REPLO_ATTRIBUTION_PROPERTY]
136
+ attributes: [...cart.attributes ?? [], REPLO_ATTRIBUTION_PROPERTY],
137
+ buyerIp
129
138
  });
130
139
  return attributedCart ?? cart;
131
140
  }
132
141
  async function updateCartLineItemAction(line, cartId) {
133
142
  const cartGateway = await getCartGateway();
134
- return await cartGateway.updateCartLineItem({ line, cartId });
143
+ return await cartGateway.updateCartLineItem({
144
+ line,
145
+ cartId,
146
+ buyerIp: await readBuyerIp()
147
+ });
135
148
  }
136
149
  async function removeCartLineItemsAction(lineIds, cartId) {
137
150
  const cartGateway = await getCartGateway();
138
- return await cartGateway.removeCartLineItems({ lineIds, cartId });
151
+ return await cartGateway.removeCartLineItems({
152
+ lineIds,
153
+ cartId,
154
+ buyerIp: await readBuyerIp()
155
+ });
139
156
  }
140
157
  async function updateCartDiscountCodesAction(discountCodes, cartId) {
141
158
  const cartGateway = await getCartGateway();
142
- return await cartGateway.updateCartDiscountCodes({ discountCodes, cartId });
159
+ return await cartGateway.updateCartDiscountCodes({
160
+ discountCodes,
161
+ cartId,
162
+ buyerIp: await readBuyerIp()
163
+ });
143
164
  }
144
165
  async function updateCartNoteAction(note, cartId) {
145
166
  const cartGateway = await getCartGateway();
146
- return await cartGateway.updateCartNote({ note, cartId });
167
+ return await cartGateway.updateCartNote({
168
+ note,
169
+ cartId,
170
+ buyerIp: await readBuyerIp()
171
+ });
147
172
  }
148
173
  async function replaceCartAttributesAction(attributes, cartId) {
149
174
  const cartGateway = await getCartGateway();
150
- return await cartGateway.replaceCartAttributes({ attributes, cartId });
175
+ return await cartGateway.replaceCartAttributes({
176
+ attributes,
177
+ cartId,
178
+ buyerIp: await readBuyerIp()
179
+ });
151
180
  }
152
181
  export {
153
182
  addToCartAction,
@@ -19,7 +19,7 @@ async function getCrossSellsAction(input) {
19
19
  if (await getCheckoutProvider() !== "stripe") {
20
20
  return [];
21
21
  }
22
- return await (await getCanopyApiGateway()).findCrossSells(input);
22
+ return await getCanopyApiGateway().findCrossSells(input);
23
23
  }
24
24
  export {
25
25
  createCheckoutSessionAction,
@@ -1,10 +1,10 @@
1
1
  "use server";
2
2
  import { getCanopyApiGateway } from "../gateways/canopy-api";
3
3
  async function updateCheckoutShippingAction(input) {
4
- return await (await getCanopyApiGateway()).updateCheckoutShipping(input);
4
+ return await getCanopyApiGateway().updateCheckoutShipping(input);
5
5
  }
6
6
  async function addCrossSellAction(input) {
7
- return await (await getCanopyApiGateway()).addCrossSell(input);
7
+ return await getCanopyApiGateway().addCrossSell(input);
8
8
  }
9
9
  export {
10
10
  addCrossSellAction,
@@ -1,6 +1,11 @@
1
1
  import type { AddToCartPayload, Cart, CartBuyerIdentity, CartLinePayload, RemoveFromCartPayload, ReplaceCartAttributesPayload, UpdateCartDiscountCodesPayload, UpdateCartLineItemPayload, UpdateCartNotePayload } from "../cart-types";
2
+ /** `buyerIp` is the shopper's IP; Shopify carts forward it, others ignore it. */
2
3
  export interface CartGateway {
3
- getCart(cartId: string): Promise<Cart | null>;
4
+ getCart(request: {
5
+ cartId: string;
6
+ } & {
7
+ buyerIp: string | null;
8
+ }): Promise<Cart | null>;
4
9
  /**
5
10
  * Bring a stored cart back in line with its source of truth. Cookie carts
6
11
  * reprice against the catalog and drop unavailable lines (persisting the
@@ -8,7 +13,7 @@ export interface CartGateway {
8
13
  * cart unchanged.
9
14
  */
10
15
  reconcileStoredCart(cart: Cart): Promise<Cart>;
11
- createCart(options?: {
16
+ createCart(options: {
12
17
  lines?: CartLinePayload[];
13
18
  discountCodes?: string[];
14
19
  attributes?: {
@@ -16,16 +21,32 @@ export interface CartGateway {
16
21
  value: string;
17
22
  }[];
18
23
  buyerIdentity?: CartBuyerIdentity;
24
+ } & {
25
+ buyerIp: string | null;
26
+ }): Promise<Cart | null>;
27
+ addToCart(cartPayload: AddToCartPayload & {
28
+ buyerIp: string | null;
19
29
  }): Promise<Cart | null>;
20
- addToCart(cartPayload: AddToCartPayload): Promise<Cart | null>;
21
30
  updateCartBuyerIdentity(payload: {
22
31
  cartId: string;
23
32
  buyerIdentity: CartBuyerIdentity;
33
+ } & {
34
+ buyerIp: string | null;
35
+ }): Promise<Cart | null>;
36
+ updateCartLineItem(cartPayload: UpdateCartLineItemPayload & {
37
+ buyerIp: string | null;
38
+ }): Promise<Cart | null>;
39
+ removeCartLineItems(cartPayload: RemoveFromCartPayload & {
40
+ buyerIp: string | null;
41
+ }): Promise<Cart | null>;
42
+ updateCartDiscountCodes(cartPayload: UpdateCartDiscountCodesPayload & {
43
+ buyerIp: string | null;
44
+ }): Promise<Cart | null>;
45
+ updateCartNote(cartPayload: UpdateCartNotePayload & {
46
+ buyerIp: string | null;
47
+ }): Promise<Cart | null>;
48
+ replaceCartAttributes(cartPayload: ReplaceCartAttributesPayload & {
49
+ buyerIp: string | null;
24
50
  }): Promise<Cart | null>;
25
- updateCartLineItem(cartPayload: UpdateCartLineItemPayload): Promise<Cart | null>;
26
- removeCartLineItems(cartPayload: RemoveFromCartPayload): Promise<Cart | null>;
27
- updateCartDiscountCodes(cartPayload: UpdateCartDiscountCodesPayload): Promise<Cart | null>;
28
- updateCartNote(cartPayload: UpdateCartNotePayload): Promise<Cart | null>;
29
- replaceCartAttributes(cartPayload: ReplaceCartAttributesPayload): Promise<Cart | null>;
30
51
  }
31
52
  export declare function getCartGateway(): Promise<CartGateway>;
@@ -27,8 +27,10 @@ export declare abstract class CookieBasedCartGateway implements CartGateway {
27
27
  private reconcile;
28
28
  private reconcileForWrite;
29
29
  reconcileStoredCart(cart: Cart): Promise<Cart>;
30
- getCart(cartId: string): Promise<Cart | null>;
31
- createCart({ lines, }?: {
30
+ getCart({ cartId }: {
31
+ cartId: string;
32
+ }): Promise<Cart | null>;
33
+ createCart({ lines, }: {
32
34
  lines?: CartLinePayload[];
33
35
  discountCodes?: string[];
34
36
  buyerIdentity?: CartBuyerIdentity;
@@ -51,7 +51,7 @@ class CookieBasedCartGateway {
51
51
  }
52
52
  return reconciledCart;
53
53
  }
54
- async getCart(cartId) {
54
+ async getCart({ cartId }) {
55
55
  try {
56
56
  const cart = await loadCartFromCookie(this.config.cookieName);
57
57
  return cart?.id === cartId ? cart : null;
@@ -65,7 +65,7 @@ class CookieBasedCartGateway {
65
65
  }
66
66
  async createCart({
67
67
  lines
68
- } = {}) {
68
+ }) {
69
69
  try {
70
70
  const cart = await createCartWithPersistence({
71
71
  cookieName: this.config.cookieName,
@@ -146,7 +146,7 @@ class CookieBasedCartGateway {
146
146
  `${this.config.gatewayName} cart gateway does not yet support discount codes`
147
147
  );
148
148
  try {
149
- return await this.getCart(cartPayload.cartId);
149
+ return await this.getCart({ cartId: cartPayload.cartId });
150
150
  } catch (error) {
151
151
  console.warn("Failed to fetch cart for discount code update:", error);
152
152
  return null;
@@ -155,7 +155,7 @@ class CookieBasedCartGateway {
155
155
  async updateCartBuyerIdentity({
156
156
  cartId
157
157
  }) {
158
- return this.getCart(cartId);
158
+ return this.getCart({ cartId });
159
159
  }
160
160
  async updateCartNote({
161
161
  cartId,
@@ -4,18 +4,21 @@ class ResolverCartGateway {
4
4
  async reconcileStoredCart(cart) {
5
5
  return cart;
6
6
  }
7
- async getCart(cartId) {
8
- const gateway = await getCanopyApiGateway();
9
- return await gateway.cartGet({ cartId });
7
+ async getCart({
8
+ cartId,
9
+ buyerIp
10
+ }) {
11
+ return await getCanopyApiGateway().cartGet({ cartId, buyerIp });
10
12
  }
11
13
  async createCart({
12
14
  lines,
13
15
  discountCodes,
14
16
  attributes,
15
- buyerIdentity
16
- } = {}) {
17
+ buyerIdentity,
18
+ buyerIp
19
+ }) {
17
20
  try {
18
- const gateway = await getCanopyApiGateway();
21
+ const gateway = getCanopyApiGateway();
19
22
  return await gateway.cartCreate({
20
23
  lines: lines?.map((line) => ({
21
24
  merchandiseId: line.id,
@@ -28,7 +31,8 @@ class ResolverCartGateway {
28
31
  })),
29
32
  discountCodes,
30
33
  attributes,
31
- country: buyerIdentity?.country
34
+ country: buyerIdentity?.country,
35
+ buyerIp
32
36
  });
33
37
  } catch (error) {
34
38
  console.warn("Failed to create cart via canopy-api:", error);
@@ -37,9 +41,10 @@ class ResolverCartGateway {
37
41
  }
38
42
  async addToCart(cartPayload) {
39
43
  try {
40
- const gateway = await getCanopyApiGateway();
44
+ const gateway = getCanopyApiGateway();
41
45
  return await gateway.cartAddLines({
42
46
  cartId: cartPayload.cartId,
47
+ buyerIp: cartPayload.buyerIp,
43
48
  lines: cartPayload.lines.map((line) => ({
44
49
  merchandiseId: line.id,
45
50
  quantity: line.quantity,
@@ -57,9 +62,10 @@ class ResolverCartGateway {
57
62
  }
58
63
  async updateCartLineItem(cartPayload) {
59
64
  try {
60
- const gateway = await getCanopyApiGateway();
65
+ const gateway = getCanopyApiGateway();
61
66
  return await gateway.cartUpdateLines({
62
67
  cartId: cartPayload.cartId,
68
+ buyerIp: cartPayload.buyerIp,
63
69
  lines: [
64
70
  {
65
71
  lineId: cartPayload.line.id,
@@ -79,9 +85,10 @@ class ResolverCartGateway {
79
85
  }
80
86
  async removeCartLineItems(cartPayload) {
81
87
  try {
82
- const gateway = await getCanopyApiGateway();
88
+ const gateway = getCanopyApiGateway();
83
89
  return await gateway.cartRemoveLines({
84
90
  cartId: cartPayload.cartId,
91
+ buyerIp: cartPayload.buyerIp,
85
92
  lineIds: cartPayload.lineIds
86
93
  });
87
94
  } catch (error) {
@@ -91,9 +98,10 @@ class ResolverCartGateway {
91
98
  }
92
99
  async updateCartDiscountCodes(cartPayload) {
93
100
  try {
94
- const gateway = await getCanopyApiGateway();
101
+ const gateway = getCanopyApiGateway();
95
102
  return await gateway.cartUpdateDiscountCodes({
96
103
  cartId: cartPayload.cartId,
104
+ buyerIp: cartPayload.buyerIp,
97
105
  discountCodes: cartPayload.discountCodes
98
106
  });
99
107
  } catch (error) {
@@ -103,13 +111,15 @@ class ResolverCartGateway {
103
111
  }
104
112
  async updateCartBuyerIdentity({
105
113
  cartId,
106
- buyerIdentity
114
+ buyerIdentity,
115
+ buyerIp
107
116
  }) {
108
117
  try {
109
- const gateway = await getCanopyApiGateway();
118
+ const gateway = getCanopyApiGateway();
110
119
  return await gateway.cartUpdateBuyerIdentity({
111
120
  cartId,
112
- country: buyerIdentity.country
121
+ country: buyerIdentity.country,
122
+ buyerIp
113
123
  });
114
124
  } catch (error) {
115
125
  console.warn(
@@ -121,9 +131,10 @@ class ResolverCartGateway {
121
131
  }
122
132
  async updateCartNote(cartPayload) {
123
133
  try {
124
- const gateway = await getCanopyApiGateway();
134
+ const gateway = getCanopyApiGateway();
125
135
  return await gateway.cartUpdateNote({
126
136
  cartId: cartPayload.cartId,
137
+ buyerIp: cartPayload.buyerIp,
127
138
  note: cartPayload.note
128
139
  });
129
140
  } catch (error) {
@@ -133,9 +144,10 @@ class ResolverCartGateway {
133
144
  }
134
145
  async replaceCartAttributes(cartPayload) {
135
146
  try {
136
- const gateway = await getCanopyApiGateway();
147
+ const gateway = getCanopyApiGateway();
137
148
  return await gateway.cartReplaceAttributes({
138
149
  cartId: cartPayload.cartId,
150
+ buyerIp: cartPayload.buyerIp,
139
151
  attributes: cartPayload.attributes.map((attribute) => ({
140
152
  key: attribute.key,
141
153
  value: String(attribute.value)
@@ -19,7 +19,7 @@ class StripeCartGateway extends CookieBasedCartGateway {
19
19
  cartIdGenerator: stripeCartIdGenerator,
20
20
  gatewayName: "Stripe",
21
21
  reconcileLines: async (lines) => {
22
- const gateway = await getCanopyApiGateway();
22
+ const gateway = getCanopyApiGateway();
23
23
  return await gateway.reconcileCartLines({ lines });
24
24
  }
25
25
  };
@@ -37,7 +37,7 @@ async function startStripeCheckout({
37
37
  "[Replo] Stripe buy now does not support selling plans or discount codes; those options were ignored."
38
38
  );
39
39
  }
40
- const outcome = await (await getCanopyApiGateway()).createStripeCheckoutSession({
40
+ const outcome = await getCanopyApiGateway().createStripeCheckoutSession({
41
41
  checkoutAttemptId: checkoutAttemptId ?? uuidv4(),
42
42
  lines: lineItems.map((item) => ({
43
43
  variantId: item.variantId,
@@ -89,16 +89,13 @@ export type CheckoutAddress = {
89
89
  };
90
90
  export declare function shouldIncludeInactiveProducts(): boolean;
91
91
  declare class CanopyApiGateway {
92
- private apiHost;
93
- private baseUrl;
94
- constructor(apiHost: string);
95
92
  private static readonly REQUEST_TIMEOUT_MILLISECONDS;
96
- static setup(): Promise<CanopyApiGateway>;
97
93
  private _post;
98
94
  private _get;
99
- invokeLoader({ loaderKey, args, }: {
95
+ invokeLoader({ loaderKey, args, buyerIp, }: {
100
96
  loaderKey: string;
101
97
  args: Record<string, unknown>;
98
+ buyerIp: string | null;
102
99
  }): Promise<{
103
100
  data: unknown;
104
101
  meta?: {
@@ -121,10 +118,12 @@ declare class CanopyApiGateway {
121
118
  }[];
122
119
  }): Promise<ReconciledCartLine[]>;
123
120
  private _cartPost;
124
- cartGet({ cartId }: {
121
+ cartGet({ cartId, buyerIp, }: {
125
122
  cartId: string;
123
+ buyerIp: string | null;
126
124
  }): Promise<Cart | null>;
127
- cartCreate(options?: {
125
+ cartCreate({ buyerIp, ...options }: {
126
+ buyerIp: string | null;
128
127
  lines?: {
129
128
  merchandiseId: string;
130
129
  quantity: number;
@@ -141,8 +140,9 @@ declare class CanopyApiGateway {
141
140
  }[];
142
141
  country?: StorefrontCountryCode;
143
142
  }): Promise<Cart>;
144
- cartAddLines({ cartId, lines, }: {
143
+ cartAddLines({ cartId, lines, buyerIp, }: {
145
144
  cartId: string;
145
+ buyerIp: string | null;
146
146
  lines: {
147
147
  merchandiseId: string;
148
148
  quantity: number;
@@ -153,8 +153,9 @@ declare class CanopyApiGateway {
153
153
  sellingPlanId?: string | null;
154
154
  }[];
155
155
  }): Promise<Cart>;
156
- cartUpdateLines({ cartId, lines, }: {
156
+ cartUpdateLines({ cartId, lines, buyerIp, }: {
157
157
  cartId: string;
158
+ buyerIp: string | null;
158
159
  lines: {
159
160
  lineId: string;
160
161
  quantity: number;
@@ -165,24 +166,29 @@ declare class CanopyApiGateway {
165
166
  sellingPlanId?: string | null;
166
167
  }[];
167
168
  }): Promise<Cart>;
168
- cartRemoveLines({ cartId, lineIds, }: {
169
+ cartRemoveLines({ cartId, lineIds, buyerIp, }: {
169
170
  cartId: string;
171
+ buyerIp: string | null;
170
172
  lineIds: string[];
171
173
  }): Promise<Cart>;
172
- cartUpdateDiscountCodes({ cartId, discountCodes, }: {
174
+ cartUpdateDiscountCodes({ cartId, discountCodes, buyerIp, }: {
173
175
  cartId: string;
176
+ buyerIp: string | null;
174
177
  discountCodes: string[];
175
178
  }): Promise<Cart>;
176
- cartUpdateBuyerIdentity({ cartId, country, }: {
179
+ cartUpdateBuyerIdentity({ cartId, country, buyerIp, }: {
177
180
  cartId: string;
181
+ buyerIp: string | null;
178
182
  country: StorefrontCountryCode;
179
183
  }): Promise<Cart>;
180
- cartUpdateNote({ cartId, note, }: {
184
+ cartUpdateNote({ cartId, note, buyerIp, }: {
181
185
  cartId: string;
186
+ buyerIp: string | null;
182
187
  note: string;
183
188
  }): Promise<Cart>;
184
- cartReplaceAttributes({ cartId, attributes, }: {
189
+ cartReplaceAttributes({ cartId, attributes, buyerIp, }: {
185
190
  cartId: string;
191
+ buyerIp: string | null;
186
192
  attributes: {
187
193
  key: string;
188
194
  value: string;
@@ -223,5 +229,5 @@ declare class CanopyApiGateway {
223
229
  variantId: string;
224
230
  }): Promise<AddCrossSellOutcome>;
225
231
  }
226
- export declare function getCanopyApiGateway(): Promise<CanopyApiGateway>;
232
+ export declare function getCanopyApiGateway(): CanopyApiGateway;
227
233
  export {};
@@ -92,19 +92,12 @@ const addCrossSellOutcomeSchema = z.discriminatedUnion("type", [
92
92
  function shouldIncludeInactiveProducts() {
93
93
  return Boolean(process.env.IS_REPLO_SANDBOX);
94
94
  }
95
+ function buyerIpHeaders(buyerIp) {
96
+ return buyerIp ? { "x-replo-buyer-ip": buyerIp } : {};
97
+ }
95
98
  class CanopyApiGateway {
96
- apiHost;
97
- baseUrl;
98
- constructor(apiHost) {
99
- this.apiHost = apiHost;
100
- this.baseUrl = buildBaseUrl(apiHost);
101
- }
102
99
  static REQUEST_TIMEOUT_MILLISECONDS = 1e4;
103
- static async setup() {
104
- const env = await getEnv();
105
- return new CanopyApiGateway(env.CANOPY_API_HOST);
106
- }
107
- async _post(url, body) {
100
+ async _post(url, body, headers) {
108
101
  const abortController = new AbortController();
109
102
  const timeoutId = setTimeout(() => {
110
103
  abortController.abort();
@@ -112,7 +105,7 @@ class CanopyApiGateway {
112
105
  try {
113
106
  return await fetch(url, {
114
107
  method: "POST",
115
- headers: { "Content-Type": "application/json" },
108
+ headers: { "Content-Type": "application/json", ...headers },
116
109
  body: JSON.stringify(body),
117
110
  signal: abortController.signal
118
111
  });
@@ -121,7 +114,7 @@ class CanopyApiGateway {
121
114
  if (process.env.NODE_ENV !== "production") {
122
115
  message += `
123
116
 
124
- apiHost: ${this.apiHost}
117
+ apiHost: ${(await getEnv()).CANOPY_API_HOST}
125
118
  If you're running locally, make sure the canopy-api cloudflare worker is running. You may need to restart the dev server.`;
126
119
  }
127
120
  throw new UnknownCanopyApiError({ message, cause: error });
@@ -141,7 +134,7 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
141
134
  if (process.env.NODE_ENV !== "production") {
142
135
  message += `
143
136
 
144
- apiHost: ${this.apiHost}
137
+ apiHost: ${(await getEnv()).CANOPY_API_HOST}
145
138
  If you're running locally, make sure the canopy-api cloudflare worker is running. You may need to restart the dev server.`;
146
139
  }
147
140
  throw new UnknownCanopyApiError({ message, cause: error });
@@ -151,15 +144,20 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
151
144
  }
152
145
  async invokeLoader({
153
146
  loaderKey,
154
- args
147
+ args,
148
+ buyerIp
155
149
  }) {
156
150
  const env = await getEnv();
157
- const response = await this._post(`${this.baseUrl}/api/v1/loaders/invoke`, {
158
- projectId: env.PROJECT_ID,
159
- loaderKey,
160
- args,
161
- includeInactiveProducts: shouldIncludeInactiveProducts()
162
- });
151
+ const response = await this._post(
152
+ `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/loaders/invoke`,
153
+ {
154
+ projectId: env.PROJECT_ID,
155
+ loaderKey,
156
+ args,
157
+ includeInactiveProducts: shouldIncludeInactiveProducts()
158
+ },
159
+ buyerIpHeaders(buyerIp)
160
+ );
163
161
  if (!response.ok) {
164
162
  const errorBody = await response.text().catch(() => "");
165
163
  throw new CanopyApiGatewayError({
@@ -174,8 +172,9 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
174
172
  }) {
175
173
  const env = await getEnv();
176
174
  const response = await this._post(
177
- `${this.baseUrl}/api/v1/product-catalog/reconcile-cart-lines`,
178
- { projectId: env.PROJECT_ID, lines }
175
+ `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/product-catalog/reconcile-cart-lines`,
176
+ { projectId: env.PROJECT_ID, lines },
177
+ {}
179
178
  );
180
179
  if (!response.ok) {
181
180
  const errorBody = await response.text().catch(() => "");
@@ -186,9 +185,13 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
186
185
  }
187
186
  return reconcileCartLinesResponseSchema.parse(await response.json()).lines;
188
187
  }
189
- async _cartPost(path, body) {
188
+ async _cartPost({
189
+ path,
190
+ body,
191
+ buyerIp
192
+ }) {
190
193
  const env = await getEnv();
191
- const url = `${this.baseUrl}/api/v1/cart${path}`;
194
+ const url = `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/cart${path}`;
192
195
  const abortController = new AbortController();
193
196
  const timeoutId = setTimeout(() => {
194
197
  abortController.abort();
@@ -198,7 +201,8 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
198
201
  response = await fetch(url, {
199
202
  method: "POST",
200
203
  headers: {
201
- "Content-Type": "application/json"
204
+ "Content-Type": "application/json",
205
+ ...buyerIpHeaders(buyerIp)
202
206
  },
203
207
  body: JSON.stringify({ ...body, projectId: env.PROJECT_ID }),
204
208
  signal: abortController.signal
@@ -221,9 +225,12 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
221
225
  const json = await response.json();
222
226
  return cartSchema.parse(json.data);
223
227
  }
224
- async cartGet({ cartId }) {
228
+ async cartGet({
229
+ cartId,
230
+ buyerIp
231
+ }) {
225
232
  try {
226
- return await this._cartPost("/get", { cartId });
233
+ return await this._cartPost({ path: "/get", body: { cartId }, buyerIp });
227
234
  } catch (error) {
228
235
  if (error instanceof CanopyApiGatewayError && error.statusCode === 404) {
229
236
  return null;
@@ -231,55 +238,97 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
231
238
  throw error;
232
239
  }
233
240
  }
234
- async cartCreate(options) {
235
- return this._cartPost("/create", {
236
- lines: options?.lines,
237
- discountCodes: options?.discountCodes,
238
- attributes: options?.attributes,
239
- country: options?.country
241
+ async cartCreate({
242
+ buyerIp,
243
+ ...options
244
+ }) {
245
+ return this._cartPost({
246
+ path: "/create",
247
+ body: {
248
+ lines: options.lines,
249
+ discountCodes: options.discountCodes,
250
+ attributes: options.attributes,
251
+ country: options.country
252
+ },
253
+ buyerIp
240
254
  });
241
255
  }
242
256
  async cartAddLines({
243
257
  cartId,
244
- lines
258
+ lines,
259
+ buyerIp
245
260
  }) {
246
- return this._cartPost("/add-lines", { cartId, lines });
261
+ return this._cartPost({
262
+ path: "/add-lines",
263
+ body: { cartId, lines },
264
+ buyerIp
265
+ });
247
266
  }
248
267
  async cartUpdateLines({
249
268
  cartId,
250
- lines
269
+ lines,
270
+ buyerIp
251
271
  }) {
252
- return this._cartPost("/update-lines", { cartId, lines });
272
+ return this._cartPost({
273
+ path: "/update-lines",
274
+ body: { cartId, lines },
275
+ buyerIp
276
+ });
253
277
  }
254
278
  async cartRemoveLines({
255
279
  cartId,
256
- lineIds
280
+ lineIds,
281
+ buyerIp
257
282
  }) {
258
- return this._cartPost("/remove-lines", { cartId, lineIds });
283
+ return this._cartPost({
284
+ path: "/remove-lines",
285
+ body: { cartId, lineIds },
286
+ buyerIp
287
+ });
259
288
  }
260
289
  async cartUpdateDiscountCodes({
261
290
  cartId,
262
- discountCodes
291
+ discountCodes,
292
+ buyerIp
263
293
  }) {
264
- return this._cartPost("/update-discount-codes", { cartId, discountCodes });
294
+ return this._cartPost({
295
+ path: "/update-discount-codes",
296
+ body: { cartId, discountCodes },
297
+ buyerIp
298
+ });
265
299
  }
266
300
  async cartUpdateBuyerIdentity({
267
301
  cartId,
268
- country
302
+ country,
303
+ buyerIp
269
304
  }) {
270
- return this._cartPost("/update-buyer-identity", { cartId, country });
305
+ return this._cartPost({
306
+ path: "/update-buyer-identity",
307
+ body: { cartId, country },
308
+ buyerIp
309
+ });
271
310
  }
272
311
  async cartUpdateNote({
273
312
  cartId,
274
- note
313
+ note,
314
+ buyerIp
275
315
  }) {
276
- return this._cartPost("/update-note", { cartId, note });
316
+ return this._cartPost({
317
+ path: "/update-note",
318
+ body: { cartId, note },
319
+ buyerIp
320
+ });
277
321
  }
278
322
  async cartReplaceAttributes({
279
323
  cartId,
280
- attributes
324
+ attributes,
325
+ buyerIp
281
326
  }) {
282
- return this._cartPost("/replace-attributes", { cartId, attributes });
327
+ return this._cartPost({
328
+ path: "/replace-attributes",
329
+ body: { cartId, attributes },
330
+ buyerIp
331
+ });
283
332
  }
284
333
  async _stripeCheckoutPost({
285
334
  path,
@@ -288,8 +337,9 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
288
337
  }) {
289
338
  const env = await getEnv();
290
339
  const response = await this._post(
291
- `${this.baseUrl}/api/v1/stripe-checkout/${path}`,
292
- { projectId: env.PROJECT_ID, ...body }
340
+ `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/stripe-checkout/${path}`,
341
+ { projectId: env.PROJECT_ID, ...body },
342
+ {}
293
343
  );
294
344
  if (!response.ok) {
295
345
  throw new CanopyApiGatewayError({
@@ -308,7 +358,7 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
308
358
  }) {
309
359
  const env = await getEnv();
310
360
  const response = await this._post(
311
- `${this.baseUrl}/api/v1/stripe-checkout/create-session`,
361
+ `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/stripe-checkout/create-session`,
312
362
  {
313
363
  cartId: checkoutAttemptId,
314
364
  checkoutAttemptId,
@@ -317,7 +367,8 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
317
367
  successUrl,
318
368
  cancelUrl,
319
369
  ...uiMode === "form" ? { uiMode } : {}
320
- }
370
+ },
371
+ {}
321
372
  );
322
373
  if (!response.ok) {
323
374
  const restart = response.status === 409 ? restartCheckoutSchema.safeParse(await response.json()) : null;
@@ -362,8 +413,8 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
362
413
  });
363
414
  }
364
415
  }
365
- async function getCanopyApiGateway() {
366
- return CanopyApiGateway.setup();
416
+ function getCanopyApiGateway() {
417
+ return new CanopyApiGateway();
367
418
  }
368
419
  export {
369
420
  CanopyApiGatewayError,
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "2.18.2",
3
+ "version": "2.18.3",
4
4
  "branch": "HEAD",
5
- "commit": "b67d568983a09c8048085ad6e81a8d5a8195658e",
6
- "builtAt": "2026-11-03T21:12:47.031Z"
5
+ "commit": "00c3d9662c0d5daa0cfd0ef03bb3b182e0cc0652",
6
+ "builtAt": "2026-11-03T23:49:23.685Z"
7
7
  };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The shopper's IP, for canopy to pass to Shopify so it throttles and
3
+ * bot-checks each shopper separately; canopy only sees this site's server.
4
+ * Server actions only: it reads the incoming request.
5
+ */
6
+ export declare function readBuyerIp(): Promise<string | null>;
@@ -0,0 +1,7 @@
1
+ import { headers } from "next/headers";
2
+ async function readBuyerIp() {
3
+ return (await headers()).get("cf-connecting-ip");
4
+ }
5
+ export {
6
+ readBuyerIp
7
+ };
@@ -1,10 +1,15 @@
1
1
  "use server";
2
+ import { readBuyerIp } from "../lib/buyer-ip";
2
3
  import { fetchValidatedLoaderOutput } from "./loader-output";
3
4
  async function invokeLoaderAction({
4
5
  loaderKey,
5
6
  args
6
7
  }) {
7
- return fetchValidatedLoaderOutput({ loaderKey, args });
8
+ return fetchValidatedLoaderOutput({
9
+ loaderKey,
10
+ args,
11
+ buyerIp: await readBuyerIp()
12
+ });
8
13
  }
9
14
  export {
10
15
  invokeLoaderAction
@@ -49,7 +49,11 @@ async function invokeLoaderServer({
49
49
  try {
50
50
  return {
51
51
  kind: "data",
52
- data: await fetchValidatedLoaderOutput({ loaderKey, args })
52
+ data: await fetchValidatedLoaderOutput({
53
+ loaderKey,
54
+ args,
55
+ buyerIp: null
56
+ })
53
57
  };
54
58
  } catch (error) {
55
59
  if (error instanceof LoaderOutputError) {
@@ -13,7 +13,11 @@ async function invokeUntypedLoader({
13
13
  try {
14
14
  if (process.env.NEXT_RUNTIME) {
15
15
  const { fetchValidatedLoaderOutput } = await import("./loader-output");
16
- return await fetchValidatedLoaderOutput({ loaderKey, args });
16
+ return await fetchValidatedLoaderOutput({
17
+ loaderKey,
18
+ args,
19
+ buyerIp: null
20
+ });
17
21
  }
18
22
  const { invokeLoaderAction } = await import("./invoke-loader-action");
19
23
  return await invokeLoaderAction({ loaderKey, args });
@@ -25,7 +25,8 @@ export declare class LoaderOutputError extends CanopyError {
25
25
  * before any request, since JSON would drop them and canopy-api would reject
26
26
  * the rest. The log matters because `PrefetchedLoaders` swallows rejections.
27
27
  */
28
- export declare function fetchValidatedLoaderOutput({ loaderKey, args, }: {
28
+ export declare function fetchValidatedLoaderOutput({ loaderKey, args, buyerIp, }: {
29
29
  loaderKey: string;
30
30
  args: Record<string, unknown>;
31
+ buyerIp: string | null;
31
32
  }): Promise<unknown>;
@@ -92,15 +92,19 @@ class LoaderOutputError extends CanopyError {
92
92
  }
93
93
  async function fetchValidatedLoaderOutput({
94
94
  loaderKey,
95
- args
95
+ args,
96
+ buyerIp
96
97
  }) {
97
98
  const argsError = getLoaderArgsError({ loaderKey, args });
98
99
  if (argsError !== null) {
99
100
  console.error(argsError.message);
100
101
  throw argsError;
101
102
  }
102
- const gateway = await getCanopyApiGateway();
103
- const { data } = await gateway.invokeLoader({ loaderKey, args });
103
+ const { data } = await getCanopyApiGateway().invokeLoader({
104
+ loaderKey,
105
+ args,
106
+ buyerIp
107
+ });
104
108
  const knownKey = dataLoaderKeySchema.safeParse(loaderKey);
105
109
  if (!knownKey.success) {
106
110
  return data;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sdk",
3
- "version": "2.18.2",
3
+ "version": "2.18.3",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "2.18.2",
6
+ "version": "2.18.3",
7
7
  "branch": "HEAD",
8
- "commit": "b67d568983a09c8048085ad6e81a8d5a8195658e",
9
- "builtAt": "2026-11-03T21:12:47.031Z"
8
+ "commit": "00c3d9662c0d5daa0cfd0ef03bb3b182e0cc0652",
9
+ "builtAt": "2026-11-03T23:49:23.685Z"
10
10
  },
11
11
  "description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",