@replohq/sdk 1.13.6 → 1.14.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.
@@ -142,4 +142,8 @@ export declare function coerceNumberToString(value: string | number | null): str
142
142
  */
143
143
  export declare function formatFileSize(byteCount: number, decimalPlaces?: number): string;
144
144
  export declare const sleep: (ms: number) => Promise<unknown>;
145
+ export declare function sleepWithSignal({ delayMs, signal, }: {
146
+ delayMs: number;
147
+ signal: AbortSignal | null;
148
+ }): Promise<unknown>;
145
149
  export declare function errorMessage(error: unknown): string;
@@ -1,4 +1,4 @@
1
- export { coerceNumberToString, deepCloneAndMergeReplacingArrays, errorMessage, exhaustiveSwitch, formatFileSize, getFromRecordOrNull, hasOwnProperty, invariant, isEmpty, isNotNullish, isNullish, isPrimitive, mergeReplacingArrays, mergeReplacingObjects, noop, preventDefault, sleep, warning } from '../../../chunk-ZKAX2FW3.mjs';
1
+ export { coerceNumberToString, deepCloneAndMergeReplacingArrays, errorMessage, exhaustiveSwitch, formatFileSize, getFromRecordOrNull, hasOwnProperty, invariant, isEmpty, isNotNullish, isNullish, isPrimitive, mergeReplacingArrays, mergeReplacingObjects, noop, preventDefault, sleep, sleepWithSignal, warning } from '../../../chunk-HFKL4ZR5.mjs';
2
2
  export { isNumber_default as isNumber, isString_default as isString } from '../../../chunk-EUIVDITZ.mjs';
3
3
  export { isFunction_default as isFunction } from '../../../chunk-SSBIMQES.mjs';
4
4
  import '../../../chunk-UJCSKKID.mjs';
@@ -1,5 +1,4 @@
1
- import type { LegacyContentEntryStatus } from "./contentCollection";
2
- import { contentCollectionFieldSchema } from "./contentCollection";
1
+ import type { contentCollectionFieldSchema, LegacyContentEntryStatus } from "./contentCollection";
3
2
  import { z } from "zod";
4
3
  export declare const CONTENT_MANAGED_ENTRY_VALUE_KEYS: readonly ["slug", "title", "status", "author", "authorInitials", "excerpt", "coverImage", "coverImageAlt", "tags", "updatedAt", "publishedAt", "archivedAt", "seoTitle", "seoDescription", "related", "scratchpad", "backlinks"];
5
4
  export declare const contentEntryFrontmatterSchema: z.ZodObject<{
@@ -1,5 +1,5 @@
1
1
  import { ReploError } from '../../chunk-Y3AYABMK.mjs';
2
- import '../../chunk-ZKAX2FW3.mjs';
2
+ import '../../chunk-HFKL4ZR5.mjs';
3
3
  import '../../chunk-EUIVDITZ.mjs';
4
4
  import '../../chunk-SSBIMQES.mjs';
5
5
  import { __commonJS, __toESM } from '../../chunk-UJCSKKID.mjs';
@@ -1,5 +1,5 @@
1
1
  import { ReploError } from '../../../chunk-Y3AYABMK.mjs';
2
- import '../../../chunk-ZKAX2FW3.mjs';
2
+ import '../../../chunk-HFKL4ZR5.mjs';
3
3
  import '../../../chunk-EUIVDITZ.mjs';
4
4
  import '../../../chunk-SSBIMQES.mjs';
5
5
  import '../../../chunk-UJCSKKID.mjs';
@@ -1,6 +1,6 @@
1
1
  import { getParamSegment, getRouteParams, hasRouteParam, matchSource } from '../../../chunk-VPEVERWE.mjs';
2
2
  import { ReploError } from '../../../chunk-Y3AYABMK.mjs';
3
- import '../../../chunk-ZKAX2FW3.mjs';
3
+ import '../../../chunk-HFKL4ZR5.mjs';
4
4
  import '../../../chunk-EUIVDITZ.mjs';
5
5
  import '../../../chunk-SSBIMQES.mjs';
6
6
  import '../../../chunk-UJCSKKID.mjs';
@@ -59,6 +59,11 @@ export declare const cartSchema: z.ZodObject<{
59
59
  currencyCode: z.ZodString;
60
60
  }, z.core.$strip>;
61
61
  }, z.core.$strip>>>;
62
+ components: z.ZodOptional<z.ZodArray<z.ZodObject<{
63
+ merchandiseId: z.ZodString;
64
+ quantity: z.ZodNumber;
65
+ sellingPlanId: z.ZodNullable<z.ZodString>;
66
+ }, z.core.$strip>>>;
62
67
  }, z.core.$strip>>;
63
68
  cost: z.ZodObject<{
64
69
  subtotalAmount: z.ZodNumber;
@@ -147,6 +152,11 @@ export declare const clientCartSchema: z.ZodObject<{
147
152
  currencyCode: z.ZodString;
148
153
  }, z.core.$strip>;
149
154
  }, z.core.$strip>>>;
155
+ components: z.ZodOptional<z.ZodArray<z.ZodObject<{
156
+ merchandiseId: z.ZodString;
157
+ quantity: z.ZodNumber;
158
+ sellingPlanId: z.ZodNullable<z.ZodString>;
159
+ }, z.core.$strip>>>;
150
160
  merchandise: z.ZodNullable<z.ZodObject<{
151
161
  id: z.ZodString;
152
162
  title: z.ZodString;
@@ -40,12 +40,18 @@ const cartLineMerchandiseSchema = z.object({
40
40
  image: cartImageSchema.nullable(),
41
41
  selectedOptions: z.array(z.object({ name: z.string(), value: z.string() }))
42
42
  });
43
+ const cartLineComponentSchema = z.object({
44
+ merchandiseId: z.string(),
45
+ quantity: z.number(),
46
+ sellingPlanId: z.string().nullable()
47
+ });
43
48
  const cartLineSchema = z.object({
44
49
  id: z.string(),
45
50
  quantity: z.number(),
46
51
  merchandise: cartLineMerchandiseSchema,
47
52
  attributes: z.array(cartLineAttributeSchema),
48
- sellingPlanAllocation: sellingPlanAllocationSchema.nullish()
53
+ sellingPlanAllocation: sellingPlanAllocationSchema.nullish(),
54
+ components: z.array(cartLineComponentSchema).optional()
49
55
  });
50
56
  const cartCostSchema = z.object({
51
57
  subtotalAmount: amountSchema,
@@ -77,6 +77,24 @@ export interface CartLine {
77
77
  merchandise: CartLineMerchandise | null;
78
78
  attributes: CartLineAttribute[];
79
79
  sellingPlanAllocation?: SellingPlanAllocation | null;
80
+ /**
81
+ * For a Shopify bundle line, the variants it holds; empty on other lines.
82
+ * The line itself is what checkout charges, so render and price the line
83
+ * and use components only to show what is inside it.
84
+ */
85
+ components?: CartLineComponent[];
86
+ }
87
+ /**
88
+ * A variant inside a bundle line. For a bundle Shopify merged from lines the
89
+ * shopper added, these are those lines.
90
+ */
91
+ export interface CartLineComponent {
92
+ /** The component's variant ID */
93
+ merchandiseId: string;
94
+ /** How many of this variant the bundle line holds in total */
95
+ quantity: number;
96
+ /** The component's selling plan, or null for a one-time purchase */
97
+ sellingPlanId: string | null;
80
98
  }
81
99
  export interface CartCost {
82
100
  /** Integer minor units (Stripe convention) in currencyCode */
@@ -25,6 +25,20 @@ function totalsFromCart(cart) {
25
25
  }
26
26
  return totals;
27
27
  }
28
+ function totalsWithBundleComponents(cart) {
29
+ const totals = totalsFromCart(cart);
30
+ for (const line of cart.lines) {
31
+ for (const component of line.components ?? []) {
32
+ addToTotals(
33
+ totals,
34
+ component.merchandiseId,
35
+ component.sellingPlanId,
36
+ component.quantity
37
+ );
38
+ }
39
+ }
40
+ return totals;
41
+ }
28
42
  function diffTotals(intended, saved) {
29
43
  const adjustments = [];
30
44
  for (const [key, entry] of intended) {
@@ -48,11 +62,11 @@ function deriveAddToCartAdjustments({
48
62
  requestedLines,
49
63
  savedCart
50
64
  }) {
51
- const intended = totalsFromCart(cartBeforeAdd);
65
+ const intended = totalsWithBundleComponents(cartBeforeAdd);
52
66
  for (const line of requestedLines) {
53
67
  addToTotals(intended, line.id, line.sellingPlanId ?? null, line.quantity);
54
68
  }
55
- return diffTotals(intended, totalsFromCart(savedCart));
69
+ return diffTotals(intended, totalsWithBundleComponents(savedCart));
56
70
  }
57
71
  function deriveUpdateCartAdjustments({
58
72
  cartBeforeUpdate,
@@ -689,6 +689,26 @@ function formatFileSize(byteCount, decimalPlaces = 0) {
689
689
  var sleep = async (ms) => {
690
690
  return await new Promise((resolve) => setTimeout(resolve, ms));
691
691
  };
692
+ function sleepWithSignal({
693
+ delayMs,
694
+ signal
695
+ }) {
696
+ if (signal === null) {
697
+ return sleep(delayMs);
698
+ }
699
+ signal.throwIfAborted();
700
+ return new Promise((resolve, reject) => {
701
+ const onAbort = () => {
702
+ clearTimeout(timeoutId);
703
+ reject(signal.reason);
704
+ };
705
+ const timeoutId = setTimeout(() => {
706
+ signal.removeEventListener("abort", onAbort);
707
+ resolve();
708
+ }, delayMs);
709
+ signal.addEventListener("abort", onAbort, { once: true });
710
+ });
711
+ }
692
712
  function errorMessage(error) {
693
713
  if (error instanceof Error) {
694
714
  return error.message;
@@ -700,4 +720,4 @@ function errorMessage(error) {
700
720
  return String(error);
701
721
  }
702
722
 
703
- export { coerceNumberToString, deepCloneAndMergeReplacingArrays, errorMessage, exhaustiveSwitch, formatFileSize, getFromRecordOrNull, hasOwnProperty4 as hasOwnProperty, invariant, isEmpty2 as isEmpty, isNotNullish, isNullish, isPrimitive, mergeReplacingArrays, mergeReplacingObjects, noop, preventDefault, sleep, warning };
723
+ export { coerceNumberToString, deepCloneAndMergeReplacingArrays, errorMessage, exhaustiveSwitch, formatFileSize, getFromRecordOrNull, hasOwnProperty4 as hasOwnProperty, invariant, isEmpty2 as isEmpty, isNotNullish, isNullish, isPrimitive, mergeReplacingArrays, mergeReplacingObjects, noop, preventDefault, sleep, sleepWithSignal, warning };
package/content.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import "server-only";
2
2
  import type { Metadata } from "next";
3
3
  import type { ReactNode } from "react";
4
+ import type { contentCollectionFieldSchema } from "./_vendor/schemas/contentCollection";
4
5
  import type { ContentManifest } from "./_vendor/schemas/contentManifest";
5
- import { contentCollectionFieldSchema } from "./_vendor/schemas/contentCollection";
6
6
  import { z } from "zod";
7
7
  /** Public name for the shared collection-field contract. */
8
8
  export type ContentField = z.infer<typeof contentCollectionFieldSchema>;
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "1.13.6",
3
+ "version": "1.14.0",
4
4
  "branch": "HEAD",
5
- "commit": "26bb95e185c8dfd266d3635694fc84df57c83869",
6
- "builtAt": "2026-09-25T18:56:30.188Z"
5
+ "commit": "3a43b7ccb4b4143e6b0cbe1c5678c4702deb8b9d",
6
+ "builtAt": "2026-10-07T15:54:24.638Z"
7
7
  };
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * Results are cached via Next.js `unstable_cache` so repeated SSR renders
6
6
  * for the same loader+args return instantly without a network round-trip
7
- * to canopy-api. The canopy-api's own in-memory cache still acts as a second
8
- * layer for cold starts and cross-instance sharing.
7
+ * to canopy-api. Canopy can also cache opted-in loaders in shared KV when
8
+ * its deployment enables caching.
9
9
  *
10
10
  * A canopy 404 is cached as a not-found marker and rethrown as a 404
11
11
  * `CanopyApiGatewayError`, so a successful revalidation can displace a stale
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { z } from "zod";
2
+ import type { z } from "zod";
3
3
  import { judgemeProductReviewAggregateLoaderOutputSchema } from "./loader-schemas";
4
4
  type JudgemeReviewAggregateData = z.infer<typeof judgemeProductReviewAggregateLoaderOutputSchema>;
5
5
  type JudgemeReviewAggregateLoaderProps = {
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { z } from "zod";
2
+ import type { z } from "zod";
3
3
  import { judgemeProductReviewsLoaderOutputSchema } from "./loader-schemas";
4
4
  type JudgemeReviewsData = z.infer<typeof judgemeProductReviewsLoaderOutputSchema>;
5
5
  type JudgemeReviewsLoaderProps = {
@@ -371,6 +371,7 @@ export declare const productSchema: z.ZodObject<{
371
371
  externalId: z.ZodNullable<z.ZodString>;
372
372
  }, z.core.$strip>>;
373
373
  }, z.core.$strip>>>;
374
+ requiresSellingPlan: z.ZodOptional<z.ZodBoolean>;
374
375
  metafields: z.ZodOptional<z.ZodArray<z.ZodObject<{
375
376
  key: z.ZodString;
376
377
  namespace: z.ZodString;
@@ -583,6 +584,7 @@ export declare const fullProductSchema: z.ZodObject<{
583
584
  externalId: z.ZodNullable<z.ZodString>;
584
585
  }, z.core.$strip>>;
585
586
  }, z.core.$strip>>;
587
+ requiresSellingPlan: z.ZodOptional<z.ZodBoolean>;
586
588
  metafields: z.ZodArray<z.ZodObject<{
587
589
  key: z.ZodString;
588
590
  namespace: z.ZodString;
@@ -776,6 +778,7 @@ export declare const paginatedProductsSchema: z.ZodObject<{
776
778
  externalId: z.ZodNullable<z.ZodString>;
777
779
  }, z.core.$strip>>;
778
780
  }, z.core.$strip>>>;
781
+ requiresSellingPlan: z.ZodOptional<z.ZodBoolean>;
779
782
  metafields: z.ZodOptional<z.ZodArray<z.ZodObject<{
780
783
  key: z.ZodString;
781
784
  namespace: z.ZodString;
@@ -144,6 +144,7 @@ const productSchema = z.object({
144
144
  productType: z.string().nullish(),
145
145
  tags: z.array(z.string()).optional(),
146
146
  sellingPlanGroups: z.array(sellingPlanGroupSchema).optional(),
147
+ requiresSellingPlan: z.boolean().optional(),
147
148
  metafields: z.array(metafieldSchema).optional()
148
149
  });
149
150
  const okendoReviewSchema = z.object({
@@ -185,6 +186,7 @@ const fullProductSchema = z.object({
185
186
  productType: z.string().nullable(),
186
187
  tags: z.array(z.string()),
187
188
  sellingPlanGroups: z.array(sellingPlanGroupSchema),
189
+ requiresSellingPlan: productSchema.shape.requiresSellingPlan,
188
190
  metafields: z.array(metafieldSchema)
189
191
  });
190
192
  const fullCollectionSchema = z.object({
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { z } from "zod";
2
+ import type { z } from "zod";
3
3
  import { looxProductReviewAggregateLoaderOutputSchema } from "./loader-schemas";
4
4
  type LooxReviewAggregateData = z.infer<typeof looxProductReviewAggregateLoaderOutputSchema>;
5
5
  type LooxReviewAggregateLoaderProps = {
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { z } from "zod";
2
+ import type { z } from "zod";
3
3
  import { looxProductReviewsLoaderOutputSchema } from "./loader-schemas";
4
4
  type LooxReviewsData = z.infer<typeof looxProductReviewsLoaderOutputSchema>;
5
5
  type LooxReviewsLoaderProps = {
@@ -146,6 +146,7 @@ declare const rebuyRecommendationsOutputSchema: z.ZodObject<{
146
146
  externalId: z.ZodNullable<z.ZodString>;
147
147
  }, z.core.$strip>>;
148
148
  }, z.core.$strip>>>;
149
+ requiresSellingPlan: z.ZodOptional<z.ZodBoolean>;
149
150
  metafields: z.ZodOptional<z.ZodArray<z.ZodObject<{
150
151
  key: z.ZodString;
151
152
  namespace: z.ZodString;
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { z } from "zod";
2
+ import type { z } from "zod";
3
3
  import { reviewsIoProductReviewAggregateLoaderOutputSchema } from "./loader-schemas";
4
4
  type ReviewsIoReviewAggregateData = z.infer<typeof reviewsIoProductReviewAggregateLoaderOutputSchema>;
5
5
  type ReviewsIoReviewAggregateLoaderProps = {
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { z } from "zod";
2
+ import type { z } from "zod";
3
3
  import { reviewsIoProductReviewsLoaderOutputSchema } from "./loader-schemas";
4
4
  type ReviewsIoReviewsData = z.infer<typeof reviewsIoProductReviewsLoaderOutputSchema>;
5
5
  type ReviewsIoReviewsLoaderProps = {
@@ -1,20 +1,34 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { StatsigExperimentAssignment } from "./loader-schemas";
3
- type StatsigExperimentLoaderProps = {
4
- loaderKey: string;
5
- experimentName: string;
3
+ type StatsigVisitorIds = {
6
4
  /**
7
- * Durable per-visitor identifier. Read it from the `statsig_stable_id` cookie
8
- * in the page's server component and pass the same value to both
9
- * `PrefetchedLoaders` and this component so the SSR-prefetched assignment
10
- * hydrates without a refetch and stays consistent across renders.
5
+ * Shorthand for `customIds.stableID`. Use it only when the experiment's ID
6
+ * type is `stableID`.
11
7
  */
12
- stableId: string;
8
+ stableId?: string;
13
9
  userId?: string;
10
+ /**
11
+ * Visitor IDs keyed by the experiment's Statsig ID type, e.g.
12
+ * `{ publicID: visitorId }`. Read the ID in the page's server component and
13
+ * pass the same value to both `PrefetchedLoaders` and this component so the
14
+ * SSR-prefetched assignment hydrates without a refetch.
15
+ */
16
+ customIds?: Record<string, string>;
17
+ };
18
+ type AtLeastOneVisitorId = (StatsigVisitorIds & {
19
+ stableId: string;
20
+ }) | (StatsigVisitorIds & {
21
+ userId: string;
22
+ }) | (StatsigVisitorIds & {
23
+ customIds: Record<string, string>;
24
+ });
25
+ type StatsigExperimentLoaderProps = AtLeastOneVisitorId & {
26
+ loaderKey: string;
27
+ experimentName: string;
14
28
  environmentTier?: string;
15
29
  customAttributes?: Record<string, string>;
16
30
  children: (assignment: StatsigExperimentAssignment) => ReactNode;
17
31
  fallback?: ReactNode;
18
32
  };
19
- export declare function StatsigExperimentLoader({ loaderKey, experimentName, stableId, userId, environmentTier, customAttributes, children, fallback, }: StatsigExperimentLoaderProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function StatsigExperimentLoader({ loaderKey, experimentName, stableId, userId, customIds, environmentTier, customAttributes, children, fallback, }: StatsigExperimentLoaderProps): import("react/jsx-runtime").JSX.Element;
20
34
  export {};
@@ -9,13 +9,15 @@ function buildArgs({
9
9
  experimentName,
10
10
  stableId,
11
11
  userId,
12
+ customIds,
12
13
  environmentTier,
13
14
  customAttributes
14
15
  }) {
15
16
  return {
16
17
  experimentName,
17
- stableId,
18
+ ...stableId !== void 0 ? { stableId } : {},
18
19
  ...userId !== void 0 ? { userId } : {},
20
+ ...customIds !== void 0 ? { customIds } : {},
19
21
  ...environmentTier !== void 0 ? { environmentTier } : {},
20
22
  ...customAttributes !== void 0 ? { customAttributes } : {}
21
23
  };
@@ -25,6 +27,7 @@ function StatsigExperimentLoaderInner({
25
27
  experimentName,
26
28
  stableId,
27
29
  userId,
30
+ customIds,
28
31
  environmentTier,
29
32
  customAttributes,
30
33
  children
@@ -33,6 +36,7 @@ function StatsigExperimentLoaderInner({
33
36
  experimentName,
34
37
  stableId,
35
38
  userId,
39
+ customIds,
36
40
  environmentTier,
37
41
  customAttributes
38
42
  });
@@ -50,6 +54,7 @@ function StatsigExperimentLoader({
50
54
  experimentName,
51
55
  stableId,
52
56
  userId,
57
+ customIds,
53
58
  environmentTier,
54
59
  customAttributes,
55
60
  children,
@@ -59,7 +64,7 @@ function StatsigExperimentLoader({
59
64
  LoaderErrorBoundary,
60
65
  {
61
66
  fallback: fallback ?? null,
62
- resetKey: `${loaderKey}:${experimentName}:${stableId}`,
67
+ resetKey: `${loaderKey}:${experimentName}:${JSON.stringify({ stableId, userId, customIds })}`,
63
68
  children: /* @__PURE__ */ jsx(Suspense, { fallback: fallback ?? null, children: /* @__PURE__ */ jsx(
64
69
  StatsigExperimentLoaderInner,
65
70
  {
@@ -67,6 +72,7 @@ function StatsigExperimentLoader({
67
72
  experimentName,
68
73
  stableId,
69
74
  userId,
75
+ customIds,
70
76
  environmentTier,
71
77
  customAttributes,
72
78
  children
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { z } from "zod";
2
+ import type { z } from "zod";
3
3
  import { yotpoProductReviewAggregateLoaderOutputSchema } from "./loader-schemas";
4
4
  type YotpoReviewAggregateData = z.infer<typeof yotpoProductReviewAggregateLoaderOutputSchema>;
5
5
  type YotpoReviewAggregateLoaderProps = {
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { z } from "zod";
2
+ import type { z } from "zod";
3
3
  import { yotpoProductReviewsLoaderOutputSchema } from "./loader-schemas";
4
4
  type YotpoReviewsData = z.infer<typeof yotpoProductReviewsLoaderOutputSchema>;
5
5
  type YotpoReviewsLoaderProps = {
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sdk",
3
- "version": "1.13.6",
3
+ "version": "1.14.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "1.13.6",
6
+ "version": "1.14.0",
7
7
  "branch": "HEAD",
8
- "commit": "26bb95e185c8dfd266d3635694fc84df57c83869",
9
- "builtAt": "2026-09-25T18:56:30.188Z"
8
+ "commit": "3a43b7ccb4b4143e6b0cbe1c5678c4702deb8b9d",
9
+ "builtAt": "2026-10-07T15:54:24.638Z"
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",
@@ -261,6 +261,10 @@
261
261
  "types": "./routing/geo.d.ts",
262
262
  "default": "./routing/geo.js"
263
263
  },
264
+ "./routing/visitor-id": {
265
+ "types": "./routing/visitor-id.d.ts",
266
+ "default": "./routing/visitor-id.js"
267
+ },
264
268
  "./_vendor/schemas/loaderKeys": {
265
269
  "types": "./_vendor/schemas/loaderKeys.d.ts",
266
270
  "default": "./_vendor/schemas/loaderKeys.mjs"
@@ -0,0 +1,41 @@
1
+ import type { NextRequest, NextResponse } from "next/server";
2
+ import type { ConsentCategory } from "../consent/types";
3
+ /**
4
+ * Gives a first-time visitor a durable visitor ID (a UUID) in the `cookieName`
5
+ * cookie, so experiments bucket every page view of theirs the same way. Call it
6
+ * in `middleware.ts` on the response the rest of the middleware returns. It
7
+ * never replaces an existing cookie, so a visitor keeps any ID the customer's
8
+ * own site already gave them, and it skips redirects.
9
+ *
10
+ * When the request's host is `cookieDomain` or one of its subdomains, the
11
+ * cookie is scoped to `cookieDomain` so the customer's other subdomains (their
12
+ * funnel or checkout) see the same ID. On any other host, such as a preview or
13
+ * a `replosites.com` address, or with `cookieDomain: null`, the cookie stays on
14
+ * the current host.
15
+ *
16
+ * With a `requiredConsent` category, the ID is only created once the visitor
17
+ * has granted that category in Replo's consent banner. Pass `null` when the
18
+ * site needs no consent for it.
19
+ */
20
+ export declare function ensureVisitorId({ request, response, cookieName, cookieDomain, requiredConsent, }: {
21
+ request: NextRequest;
22
+ response: NextResponse;
23
+ cookieName: string;
24
+ cookieDomain: string | null;
25
+ requiredConsent: VisitorIdConsent;
26
+ }): NextResponse;
27
+ /**
28
+ * Reads the visitor ID for an experiment evaluation, or `null` when the
29
+ * visitor has none or hasn't granted `requiredConsent`. Pass it the page's
30
+ * `await cookies()` and the same `cookieName` and `requiredConsent` as
31
+ * `ensureVisitorId`, so a visitor who withdraws consent sees the default and
32
+ * logs no exposure.
33
+ */
34
+ export declare function findVisitorId({ cookies, cookieName, requiredConsent, }: {
35
+ cookies: VisitorIdCookies;
36
+ cookieName: string;
37
+ requiredConsent: VisitorIdConsent;
38
+ }): string | null;
39
+ type VisitorIdCookies = Pick<NextRequest["cookies"], "get">;
40
+ type VisitorIdConsent = Exclude<ConsentCategory, "necessary"> | null;
41
+ export {};
@@ -0,0 +1,97 @@
1
+ import { CONSENT_COOKIE_NAME, parseConsentCookie } from "../consent/cookie";
2
+ const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
3
+ const SHARED_REPLO_DOMAINS = /* @__PURE__ */ new Set([
4
+ "reploshops.com",
5
+ "replosites.com",
6
+ "replosites-staging.com",
7
+ "replosites-dev.com"
8
+ ]);
9
+ const REGISTRY_SUFFIX_LABELS = /* @__PURE__ */ new Set([
10
+ "ac",
11
+ "co",
12
+ "com",
13
+ "edu",
14
+ "gov",
15
+ "ne",
16
+ "net",
17
+ "or",
18
+ "org"
19
+ ]);
20
+ function ensureVisitorId({
21
+ request,
22
+ response,
23
+ cookieName,
24
+ cookieDomain,
25
+ requiredConsent
26
+ }) {
27
+ const isRedirect = response.status >= 300 && response.status < 400;
28
+ const isAlreadySet = response.headers.getSetCookie().some((setCookie) => {
29
+ return setCookie.startsWith(`${cookieName}=`);
30
+ });
31
+ if (isRedirect || isAlreadySet || !isConsentGranted({ cookies: request.cookies, requiredConsent }) || request.cookies.get(cookieName)?.value) {
32
+ return response;
33
+ }
34
+ const appendedSetCookies = response.headers.getSetCookie().filter((setCookie) => {
35
+ return !response.cookies.has(setCookie.split("=")[0]?.trim() ?? "");
36
+ });
37
+ const domain = findCookieDomain({
38
+ hostname: request.nextUrl.hostname,
39
+ cookieDomain
40
+ });
41
+ response.cookies.set({
42
+ name: cookieName,
43
+ value: crypto.randomUUID(),
44
+ path: "/",
45
+ maxAge: ONE_YEAR_SECONDS,
46
+ sameSite: "lax",
47
+ secure: request.nextUrl.protocol === "https:",
48
+ httpOnly: false,
49
+ ...domain !== null ? { domain } : {}
50
+ });
51
+ for (const setCookie of appendedSetCookies) {
52
+ response.headers.append("set-cookie", setCookie);
53
+ }
54
+ response.headers.set("Cache-Control", "private, no-store");
55
+ return response;
56
+ }
57
+ function findVisitorId({
58
+ cookies,
59
+ cookieName,
60
+ requiredConsent
61
+ }) {
62
+ const visitorId = cookies.get(cookieName)?.value;
63
+ if (!visitorId || !isConsentGranted({ cookies, requiredConsent })) {
64
+ return null;
65
+ }
66
+ return visitorId;
67
+ }
68
+ function isConsentGranted({
69
+ cookies,
70
+ requiredConsent
71
+ }) {
72
+ if (requiredConsent === null) {
73
+ return true;
74
+ }
75
+ const consent = parseConsentCookie(cookies.get(CONSENT_COOKIE_NAME)?.value);
76
+ return consent?.categories[requiredConsent] ?? false;
77
+ }
78
+ function findCookieDomain({
79
+ hostname,
80
+ cookieDomain
81
+ }) {
82
+ if (cookieDomain === null) {
83
+ return null;
84
+ }
85
+ const domain = cookieDomain.trim().toLowerCase().replace(/^\./, "");
86
+ const labels = domain.split(".");
87
+ const isRegistrySuffix = labels.length < 2 || labels.length === 2 && REGISTRY_SUFFIX_LABELS.has(labels[0] ?? "");
88
+ if (isRegistrySuffix || SHARED_REPLO_DOMAINS.has(domain)) {
89
+ return null;
90
+ }
91
+ const host = hostname.toLowerCase();
92
+ return host === domain || host.endsWith(`.${domain}`) ? domain : null;
93
+ }
94
+ export {
95
+ ensureVisitorId,
96
+ findVisitorId
97
+ };