@replohq/sdk 2.18.2 → 2.19.0

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.
Files changed (40) hide show
  1. package/_vendor/replo-utils/lib/crypto.d.ts +5 -0
  2. package/_vendor/schemas/generated/consent.d.ts +13 -1
  3. package/analytics/analytics-provider.js +3 -1
  4. package/analytics/get-analytics-sinks.js +7 -4
  5. package/analytics/replo-pixel-script.d.ts +5 -5
  6. package/analytics/replo-pixel-script.js +23 -22
  7. package/cart/cart-actions.js +42 -13
  8. package/cart/checkout-actions.js +1 -1
  9. package/cart/checkout-session-actions.js +2 -2
  10. package/cart/gateways/cart-gateway.d.ts +29 -8
  11. package/cart/gateways/cookie-cart-gateway.d.ts +4 -2
  12. package/cart/gateways/cookie-cart-gateway.js +4 -4
  13. package/cart/gateways/resolver-cart-gateway.js +28 -16
  14. package/cart/gateways/stripe-cart-gateway.js +1 -1
  15. package/cart/start-stripe-checkout.js +1 -1
  16. package/consent/consent-platform.d.ts +20 -5
  17. package/consent/consent-platform.js +94 -11
  18. package/consent/consent-store.d.ts +7 -2
  19. package/consent/consent-store.js +16 -4
  20. package/consent/inject-script-descriptors.d.ts +8 -1
  21. package/consent/inject-script-descriptors.js +23 -0
  22. package/consent/replo-scripts.d.ts +6 -2
  23. package/consent/replo-scripts.js +35 -27
  24. package/consent/script-registration-store.d.ts +2 -0
  25. package/consent/script-snippets.d.ts +1 -1
  26. package/consent/types.d.ts +18 -4
  27. package/consent/usercentrics-consent.d.ts +15 -0
  28. package/consent/usercentrics-consent.js +96 -0
  29. package/consent/window-api.js +1 -1
  30. package/gateways/canopy-api.d.ts +21 -15
  31. package/gateways/canopy-api.js +105 -54
  32. package/lib/buildMetadata.js +3 -3
  33. package/lib/buyer-ip.d.ts +6 -0
  34. package/lib/buyer-ip.js +7 -0
  35. package/loaders/invoke-loader-action.js +6 -1
  36. package/loaders/invoke-loader-server.js +5 -1
  37. package/loaders/invoke-loader.js +5 -1
  38. package/loaders/loader-output.d.ts +2 -1
  39. package/loaders/loader-output.js +7 -3
  40. package/package.json +4 -4
@@ -0,0 +1,96 @@
1
+ import * as v from "valibot";
2
+ const usercentricsDetailsSchema = v.object({
3
+ consent: v.optional(v.object({ type: v.optional(v.string()) })),
4
+ services: v.record(
5
+ v.string(),
6
+ v.object({
7
+ name: v.string(),
8
+ essential: v.optional(v.boolean()),
9
+ consent: v.object({ given: v.boolean() })
10
+ })
11
+ )
12
+ });
13
+ const usercentricsBaseInfoSchema = v.array(
14
+ v.object({
15
+ name: v.string(),
16
+ isEssential: v.optional(v.boolean()),
17
+ consent: v.object({
18
+ status: v.boolean(),
19
+ history: v.optional(v.array(v.object({ type: v.optional(v.string()) })))
20
+ })
21
+ })
22
+ );
23
+ function usercentricsCategories(services) {
24
+ const optionalServices = services.filter((service) => !service.essential);
25
+ const acceptedAll = optionalServices.length > 0 && optionalServices.every((service) => service.given);
26
+ return {
27
+ necessary: true,
28
+ analytics: acceptedAll,
29
+ marketing: acceptedAll,
30
+ preferences: acceptedAll,
31
+ sale_of_data: acceptedAll
32
+ };
33
+ }
34
+ async function readUsercentricsServices() {
35
+ if (window.__ucCmp?.getConsentDetails) {
36
+ const details = v.safeParse(
37
+ usercentricsDetailsSchema,
38
+ await window.__ucCmp.getConsentDetails()
39
+ );
40
+ if (!details.success) {
41
+ console.warn("[Replo] Unreadable Usercentrics consent details");
42
+ return null;
43
+ }
44
+ return {
45
+ services: Object.values(details.output.services).map((service) => ({
46
+ name: service.name,
47
+ given: service.consent.given,
48
+ essential: service.essential === true
49
+ })),
50
+ isExplicit: details.output.consent?.type === "EXPLICIT"
51
+ };
52
+ }
53
+ const rawBaseInfo = window.UC_UI?.getServicesBaseInfo?.();
54
+ if (rawBaseInfo === void 0) {
55
+ return null;
56
+ }
57
+ const baseInfo = v.safeParse(usercentricsBaseInfoSchema, rawBaseInfo);
58
+ if (!baseInfo.success) {
59
+ console.warn("[Replo] Unreadable Usercentrics service info");
60
+ return null;
61
+ }
62
+ return {
63
+ services: baseInfo.output.map((service) => ({
64
+ name: service.name,
65
+ given: service.consent.status,
66
+ essential: service.isEssential === true
67
+ })),
68
+ isExplicit: baseInfo.output.some((service) => {
69
+ return service.consent.history?.at(-1)?.type === "explicit";
70
+ })
71
+ };
72
+ }
73
+ async function readUsercentricsChange(previous) {
74
+ const answer = await readUsercentricsServices();
75
+ if (!answer) {
76
+ return null;
77
+ }
78
+ const categories = usercentricsCategories(answer.services);
79
+ const grantedUsercentricsServices = answer.services.filter((service) => service.given).map((service) => service.name).sort();
80
+ const isUnchanged = categories.analytics === previous.categories.analytics && grantedUsercentricsServices.join() === previous.grantedUsercentricsServices.join();
81
+ const isDecided = answer.isExplicit || answer.services.some((service) => service.given && !service.essential);
82
+ if (isUnchanged && (previous.decidedAt !== null || !isDecided)) {
83
+ return null;
84
+ }
85
+ return {
86
+ categories,
87
+ decidedAt: isDecided ? Date.now() : null,
88
+ grantedUsercentricsServices,
89
+ hasDowngrade: previous.grantedUsercentricsServices.some((name) => {
90
+ return !grantedUsercentricsServices.includes(name);
91
+ })
92
+ };
93
+ }
94
+ export {
95
+ readUsercentricsChange
96
+ };
@@ -30,7 +30,7 @@ const api = {
30
30
  saleOfDataAllowed: () => isAllowed("sale_of_data"),
31
31
  shouldShowBanner: () => {
32
32
  const state = consentStore.getSnapshot();
33
- if (state.cmp === "cookiebot" || state.cmp === "external") {
33
+ if (state.cmp && state.cmp !== "replo") {
34
34
  return false;
35
35
  }
36
36
  return state.mode !== "off" && state.decidedAt === null;
@@ -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.19.0",
4
4
  "branch": "HEAD",
5
- "commit": "b67d568983a09c8048085ad6e81a8d5a8195658e",
6
- "builtAt": "2026-11-03T21:12:47.031Z"
5
+ "commit": "377fd6358bd4e3d0d32ff03942701a9cede4e0e9",
6
+ "builtAt": "2026-11-06T19:28:13.532Z"
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.19.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "2.18.2",
6
+ "version": "2.19.0",
7
7
  "branch": "HEAD",
8
- "commit": "b67d568983a09c8048085ad6e81a8d5a8195658e",
9
- "builtAt": "2026-11-03T21:12:47.031Z"
8
+ "commit": "377fd6358bd4e3d0d32ff03942701a9cede4e0e9",
9
+ "builtAt": "2026-11-06T19:28:13.532Z"
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",