@sonordev/site-kit 2.9.0 → 3.0.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 (61) hide show
  1. package/dist/EngageWidget-LRE4VD7O.js +14 -0
  2. package/dist/{EngageWidget-JIXUBG47.js.map → EngageWidget-LRE4VD7O.js.map} +1 -1
  3. package/dist/EngageWidget-OAOLB6ED.mjs +5 -0
  4. package/dist/{EngageWidget-4W5EM4UO.mjs.map → EngageWidget-OAOLB6ED.mjs.map} +1 -1
  5. package/dist/chunk-2FEQ6TYD.mjs +37 -0
  6. package/dist/chunk-2FEQ6TYD.mjs.map +1 -0
  7. package/dist/{chunk-5QLM6LZ2.mjs → chunk-APRAGA5G.mjs} +3 -3
  8. package/dist/{chunk-5QLM6LZ2.mjs.map → chunk-APRAGA5G.mjs.map} +1 -1
  9. package/dist/{chunk-HANRPNKG.js → chunk-FMI65E5Y.js} +3 -3
  10. package/dist/{chunk-HANRPNKG.js.map → chunk-FMI65E5Y.js.map} +1 -1
  11. package/dist/{chunk-SLJJJWMV.js → chunk-FPU2QPO4.js} +338 -33
  12. package/dist/chunk-FPU2QPO4.js.map +1 -0
  13. package/dist/chunk-MLJXELPA.js +693 -0
  14. package/dist/chunk-MLJXELPA.js.map +1 -0
  15. package/dist/{chunk-IAV32YHL.mjs → chunk-RV45YMBJ.mjs} +338 -33
  16. package/dist/chunk-RV45YMBJ.mjs.map +1 -0
  17. package/dist/chunk-TCVHI3JP.mjs +686 -0
  18. package/dist/chunk-TCVHI3JP.mjs.map +1 -0
  19. package/dist/chunk-YBKALLBH.js +46 -0
  20. package/dist/chunk-YBKALLBH.js.map +1 -0
  21. package/dist/engage/index.d.mts +2 -2
  22. package/dist/engage/index.d.ts +2 -2
  23. package/dist/engage/index.js +5 -4
  24. package/dist/engage/index.js.map +1 -1
  25. package/dist/engage/index.mjs +2 -1
  26. package/dist/engage/index.mjs.map +1 -1
  27. package/dist/forms/index.js +19 -683
  28. package/dist/forms/index.js.map +1 -1
  29. package/dist/forms/index.mjs +4 -676
  30. package/dist/forms/index.mjs.map +1 -1
  31. package/dist/index.d.mts +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/layout/client.js +2 -2
  34. package/dist/layout/client.mjs +1 -1
  35. package/dist/layout/index.js +2 -2
  36. package/dist/layout/index.mjs +1 -1
  37. package/dist/middleware/index.d.mts +62 -1
  38. package/dist/middleware/index.d.ts +62 -1
  39. package/dist/middleware/index.js +105 -0
  40. package/dist/middleware/index.js.map +1 -1
  41. package/dist/middleware/index.mjs +103 -1
  42. package/dist/middleware/index.mjs.map +1 -1
  43. package/dist/slots/contract.d.mts +83 -0
  44. package/dist/slots/contract.d.ts +83 -0
  45. package/dist/slots/contract.js +41 -0
  46. package/dist/slots/contract.js.map +1 -0
  47. package/dist/slots/contract.mjs +4 -0
  48. package/dist/slots/contract.mjs.map +1 -0
  49. package/dist/slots/index.d.mts +104 -0
  50. package/dist/slots/index.d.ts +104 -0
  51. package/dist/slots/index.js +186 -0
  52. package/dist/slots/index.js.map +1 -0
  53. package/dist/slots/index.mjs +146 -0
  54. package/dist/slots/index.mjs.map +1 -0
  55. package/dist/{types-BnCwwUX3.d.ts → types-D_1iUxOQ.d.mts} +18 -1
  56. package/dist/{types-BnCwwUX3.d.mts → types-D_1iUxOQ.d.ts} +18 -1
  57. package/package.json +11 -1
  58. package/dist/EngageWidget-4W5EM4UO.mjs +0 -4
  59. package/dist/EngageWidget-JIXUBG47.js +0 -13
  60. package/dist/chunk-IAV32YHL.mjs.map +0 -1
  61. package/dist/chunk-SLJJJWMV.js.map +0 -1
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { b as ManagedFAQData, M as ManagedMetadataResult, a as ManagedSchemaProps } from './types-DPsbBKiu.mjs';
2
2
  export { c as AnalyticsConfig, b as AnalyticsEvent, P as PageView } from './types-DNSYU7qI.mjs';
3
- export { C as ChatConfig, c as EngageElement, d as WidgetConfig } from './types-BnCwwUX3.mjs';
3
+ export { C as ChatConfig, c as EngageElement, d as WidgetConfig } from './types-D_1iUxOQ.mjs';
4
4
  export { a as FormField, F as FormSubmission, M as ManagedFormConfig } from './types-0dmq3k20.mjs';
5
5
  export { a as BlogAuthor, b as BlogCategory, B as BlogPostType } from './types-5P5B9RgV.mjs';
6
6
  export { d as Cart, e as CartItem, a as CommerceCategory, C as CommerceOffering, c as CommerceSchedule, b as CommerceVariant, O as OfferingType } from './types-iZM4Lz71.mjs';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { b as ManagedFAQData, M as ManagedMetadataResult, a as ManagedSchemaProps } from './types-DPsbBKiu.js';
2
2
  export { c as AnalyticsConfig, b as AnalyticsEvent, P as PageView } from './types-DNSYU7qI.js';
3
- export { C as ChatConfig, c as EngageElement, d as WidgetConfig } from './types-BnCwwUX3.js';
3
+ export { C as ChatConfig, c as EngageElement, d as WidgetConfig } from './types-D_1iUxOQ.js';
4
4
  export { a as FormField, F as FormSubmission, M as ManagedFormConfig } from './types-0dmq3k20.js';
5
5
  export { a as BlogAuthor, b as BlogCategory, B as BlogPostType } from './types-5P5B9RgV.js';
6
6
  export { d as Cart, e as CartItem, a as CommerceCategory, C as CommerceOffering, c as CommerceSchedule, b as CommerceVariant, O as OfferingType } from './types-iZM4Lz71.js';
@@ -1,14 +1,14 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkHANRPNKG_js = require('../chunk-HANRPNKG.js');
4
+ var chunkFMI65E5Y_js = require('../chunk-FMI65E5Y.js');
5
5
  require('../chunk-ZSMWDLMK.js');
6
6
 
7
7
 
8
8
 
9
9
  Object.defineProperty(exports, "SiteKitClientProviders", {
10
10
  enumerable: true,
11
- get: function () { return chunkHANRPNKG_js.SiteKitClientProviders; }
11
+ get: function () { return chunkFMI65E5Y_js.SiteKitClientProviders; }
12
12
  });
13
13
  //# sourceMappingURL=client.js.map
14
14
  //# sourceMappingURL=client.js.map
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- export { SiteKitClientProviders } from '../chunk-5QLM6LZ2.mjs';
2
+ export { SiteKitClientProviders } from '../chunk-APRAGA5G.mjs';
3
3
  import '../chunk-4XPGGLVP.mjs';
4
4
  //# sourceMappingURL=client.mjs.map
5
5
  //# sourceMappingURL=client.mjs.map
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkHANRPNKG_js = require('../chunk-HANRPNKG.js');
3
+ var chunkFMI65E5Y_js = require('../chunk-FMI65E5Y.js');
4
4
  var chunkGCPPXUAI_js = require('../chunk-GCPPXUAI.js');
5
5
  var chunkJ25JPU77_js = require('../chunk-J25JPU77.js');
6
6
  require('../chunk-VU4GL5ZG.js');
@@ -42,7 +42,7 @@ async function SiteKitLayout({
42
42
  favicon && /* @__PURE__ */ jsxRuntime.jsx(chunkGCPPXUAI_js.ManagedFavicon, { apiKey: resolvedApiKey, apiUrl: resolvedApiUrl }),
43
43
  managedScripts && /* @__PURE__ */ jsxRuntime.jsx(chunkJ25JPU77_js.ManagedScripts, { position: "head" }),
44
44
  /* @__PURE__ */ jsxRuntime.jsxs(
45
- chunkHANRPNKG_js.SiteKitClientProviders,
45
+ chunkFMI65E5Y_js.SiteKitClientProviders,
46
46
  {
47
47
  apiKey: resolvedApiKey,
48
48
  apiUrl: resolvedApiUrl,
@@ -1,4 +1,4 @@
1
- import { SiteKitClientProviders } from '../chunk-5QLM6LZ2.mjs';
1
+ import { SiteKitClientProviders } from '../chunk-APRAGA5G.mjs';
2
2
  import { ManagedFavicon } from '../chunk-CU6OGBKK.mjs';
3
3
  import { ManagedScripts } from '../chunk-QDKQZ5XH.mjs';
4
4
  import '../chunk-TUFP3FGI.mjs';
@@ -2,6 +2,44 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { RedirectConfig } from '../redirects/index.mjs';
3
3
  import { S as SecurityHeadersConfig } from '../securityHeaders-nwZ6nP4g.mjs';
4
4
 
5
+ /**
6
+ * @sonordev/site-kit/middleware — visitor segment resolution (the "one edge pass").
7
+ *
8
+ * Pure, edge-runtime-safe helpers that classify a request into a coarse visitor
9
+ * segment (new-vs-returning + marketing source + campaign) from the URL's UTM
10
+ * params and the referrer. The middleware writes the result to the `_sk_seg`
11
+ * cookie so server-rendered slots / personalization can vary by segment WITHOUT
12
+ * each one re-parsing the request. Resolved once, at the edge.
13
+ *
14
+ * Kept side-effect-free (no NextResponse, no cookie writes) so it is trivially
15
+ * unit-testable and reusable.
16
+ */
17
+
18
+ type SegmentSource = 'paid' | 'organic' | 'social' | 'referral' | 'direct';
19
+ interface VisitorSegment {
20
+ /** `new` on the first visit (no prior `_sk_vid` cookie), else `returning`. */
21
+ visitorType: 'new' | 'returning';
22
+ /** Marketing source bucket derived from UTM medium/source, then referrer. */
23
+ source: SegmentSource;
24
+ /** `utm_campaign`, if present (sanitized when encoded). */
25
+ campaign: string | null;
26
+ }
27
+ /**
28
+ * Classify `request` into a {@link VisitorSegment}. UTM params win over the
29
+ * referrer; a same-host referrer is ignored (internal navigation, not a source).
30
+ */
31
+ declare function resolveVisitorSegment(request: NextRequest, opts: {
32
+ hadVisitorCookie: boolean;
33
+ }): VisitorSegment;
34
+ /**
35
+ * Compact, cookie-safe encoding: `"<visitorType>|<source>|<campaign>"`, e.g.
36
+ * `"new|paid|spring_sale"`. Campaign is sanitized to `[A-Za-z0-9_-]` and capped.
37
+ * Decode with {@link decodeSegment}.
38
+ */
39
+ declare function encodeSegment(seg: VisitorSegment): string;
40
+ /** Parse a `_sk_seg` cookie value produced by {@link encodeSegment}. */
41
+ declare function decodeSegment(value: string | undefined | null): VisitorSegment | null;
42
+
5
43
  /**
6
44
  * @sonordev/site-kit/middleware
7
45
  *
@@ -30,6 +68,16 @@ import { S as SecurityHeadersConfig } from '../securityHeaders-nwZ6nP4g.mjs';
30
68
  * ```
31
69
  */
32
70
 
71
+ interface IdentityConfig {
72
+ /**
73
+ * Also resolve + persist a coarse visitor segment (new-vs-returning + source
74
+ * + campaign) into the `_sk_seg` cookie, for segment-aware slots /
75
+ * personalization. Default: true (when identity is enabled).
76
+ */
77
+ segment?: boolean;
78
+ /** Days the `_sk_vid` visitor cookie persists. Default: 730 (~2 years). */
79
+ cookieMaxAgeDays?: number;
80
+ }
33
81
  interface LlmsDiscoveryConfig {
34
82
  /** Canonical site origin, e.g. `https://example.com` */
35
83
  siteUrl: string;
@@ -47,6 +95,19 @@ interface SiteKitMiddlewareConfig {
47
95
  * Preferred over visible footer links.
48
96
  */
49
97
  llmsDiscovery?: LlmsDiscoveryConfig | false;
98
+ /**
99
+ * Opt-in EDGE IDENTITY pass. When enabled, the middleware ensures a
100
+ * first-party `_sk_vid` visitor cookie exists (server-authoritative id) and
101
+ * resolves the visitor segment into `_sk_seg`, so server-rendered slots and
102
+ * personalization can read identity + segment without re-parsing the request.
103
+ * Purely additive — it only writes cookies, never changes the rendered HTML,
104
+ * so it is LCP/static-safe. Default: OFF (sites opt in for 3.0 personalization).
105
+ *
106
+ * NOTE: to make these the SAME id the client uses, the shared identity helper
107
+ * must read `_sk_vid` cookie-first (a follow-on change validated with a live
108
+ * preview); until then the cookie is the server-side mirror.
109
+ */
110
+ identity?: boolean | IdentityConfig;
50
111
  /**
51
112
  * Hook that runs BEFORE redirect check.
52
113
  * Return a NextResponse to short-circuit, or undefined to continue.
@@ -74,4 +135,4 @@ declare const siteKitMatcher: {
74
135
  /** Next.js 16+ alias — same as createMiddleware, named for the proxy convention. */
75
136
  declare const createProxy: typeof createMiddleware;
76
137
 
77
- export { type LlmsDiscoveryConfig, SecurityHeadersConfig, type SiteKitMiddlewareConfig, createMiddleware, createProxy, siteKitMatcher };
138
+ export { type IdentityConfig, type LlmsDiscoveryConfig, SecurityHeadersConfig, type SegmentSource, type SiteKitMiddlewareConfig, type VisitorSegment, createMiddleware, createProxy, decodeSegment, encodeSegment, resolveVisitorSegment, siteKitMatcher };
@@ -2,6 +2,44 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { RedirectConfig } from '../redirects/index.js';
3
3
  import { S as SecurityHeadersConfig } from '../securityHeaders-nwZ6nP4g.js';
4
4
 
5
+ /**
6
+ * @sonordev/site-kit/middleware — visitor segment resolution (the "one edge pass").
7
+ *
8
+ * Pure, edge-runtime-safe helpers that classify a request into a coarse visitor
9
+ * segment (new-vs-returning + marketing source + campaign) from the URL's UTM
10
+ * params and the referrer. The middleware writes the result to the `_sk_seg`
11
+ * cookie so server-rendered slots / personalization can vary by segment WITHOUT
12
+ * each one re-parsing the request. Resolved once, at the edge.
13
+ *
14
+ * Kept side-effect-free (no NextResponse, no cookie writes) so it is trivially
15
+ * unit-testable and reusable.
16
+ */
17
+
18
+ type SegmentSource = 'paid' | 'organic' | 'social' | 'referral' | 'direct';
19
+ interface VisitorSegment {
20
+ /** `new` on the first visit (no prior `_sk_vid` cookie), else `returning`. */
21
+ visitorType: 'new' | 'returning';
22
+ /** Marketing source bucket derived from UTM medium/source, then referrer. */
23
+ source: SegmentSource;
24
+ /** `utm_campaign`, if present (sanitized when encoded). */
25
+ campaign: string | null;
26
+ }
27
+ /**
28
+ * Classify `request` into a {@link VisitorSegment}. UTM params win over the
29
+ * referrer; a same-host referrer is ignored (internal navigation, not a source).
30
+ */
31
+ declare function resolveVisitorSegment(request: NextRequest, opts: {
32
+ hadVisitorCookie: boolean;
33
+ }): VisitorSegment;
34
+ /**
35
+ * Compact, cookie-safe encoding: `"<visitorType>|<source>|<campaign>"`, e.g.
36
+ * `"new|paid|spring_sale"`. Campaign is sanitized to `[A-Za-z0-9_-]` and capped.
37
+ * Decode with {@link decodeSegment}.
38
+ */
39
+ declare function encodeSegment(seg: VisitorSegment): string;
40
+ /** Parse a `_sk_seg` cookie value produced by {@link encodeSegment}. */
41
+ declare function decodeSegment(value: string | undefined | null): VisitorSegment | null;
42
+
5
43
  /**
6
44
  * @sonordev/site-kit/middleware
7
45
  *
@@ -30,6 +68,16 @@ import { S as SecurityHeadersConfig } from '../securityHeaders-nwZ6nP4g.js';
30
68
  * ```
31
69
  */
32
70
 
71
+ interface IdentityConfig {
72
+ /**
73
+ * Also resolve + persist a coarse visitor segment (new-vs-returning + source
74
+ * + campaign) into the `_sk_seg` cookie, for segment-aware slots /
75
+ * personalization. Default: true (when identity is enabled).
76
+ */
77
+ segment?: boolean;
78
+ /** Days the `_sk_vid` visitor cookie persists. Default: 730 (~2 years). */
79
+ cookieMaxAgeDays?: number;
80
+ }
33
81
  interface LlmsDiscoveryConfig {
34
82
  /** Canonical site origin, e.g. `https://example.com` */
35
83
  siteUrl: string;
@@ -47,6 +95,19 @@ interface SiteKitMiddlewareConfig {
47
95
  * Preferred over visible footer links.
48
96
  */
49
97
  llmsDiscovery?: LlmsDiscoveryConfig | false;
98
+ /**
99
+ * Opt-in EDGE IDENTITY pass. When enabled, the middleware ensures a
100
+ * first-party `_sk_vid` visitor cookie exists (server-authoritative id) and
101
+ * resolves the visitor segment into `_sk_seg`, so server-rendered slots and
102
+ * personalization can read identity + segment without re-parsing the request.
103
+ * Purely additive — it only writes cookies, never changes the rendered HTML,
104
+ * so it is LCP/static-safe. Default: OFF (sites opt in for 3.0 personalization).
105
+ *
106
+ * NOTE: to make these the SAME id the client uses, the shared identity helper
107
+ * must read `_sk_vid` cookie-first (a follow-on change validated with a live
108
+ * preview); until then the cookie is the server-side mirror.
109
+ */
110
+ identity?: boolean | IdentityConfig;
50
111
  /**
51
112
  * Hook that runs BEFORE redirect check.
52
113
  * Return a NextResponse to short-circuit, or undefined to continue.
@@ -74,4 +135,4 @@ declare const siteKitMatcher: {
74
135
  /** Next.js 16+ alias — same as createMiddleware, named for the proxy convention. */
75
136
  declare const createProxy: typeof createMiddleware;
76
137
 
77
- export { type LlmsDiscoveryConfig, SecurityHeadersConfig, type SiteKitMiddlewareConfig, createMiddleware, createProxy, siteKitMatcher };
138
+ export { type IdentityConfig, type LlmsDiscoveryConfig, SecurityHeadersConfig, type SegmentSource, type SiteKitMiddlewareConfig, type VisitorSegment, createMiddleware, createProxy, decodeSegment, encodeSegment, resolveVisitorSegment, siteKitMatcher };
@@ -7,6 +7,76 @@ require('../chunk-JP5WGL75.js');
7
7
  require('../chunk-ZSMWDLMK.js');
8
8
  var server = require('next/server');
9
9
 
10
+ // src/middleware/segment.ts
11
+ var PAID_MEDIUMS = ["cpc", "ppc", "paid", "paidsearch", "paid-search", "display", "cpm", "banner"];
12
+ var SOCIAL_MEDIUMS = ["social", "social-media", "social_media", "sm"];
13
+ var SOCIAL_HOSTS = [
14
+ "facebook.",
15
+ "instagram.",
16
+ "linkedin.",
17
+ "t.co",
18
+ "twitter.",
19
+ "x.com",
20
+ "pinterest.",
21
+ "tiktok.",
22
+ "youtube.",
23
+ "reddit."
24
+ ];
25
+ var SEARCH_HOSTS = ["google.", "bing.", "duckduckgo.", "yahoo.", "ecosia.", "brave.", "baidu.", "yandex."];
26
+ function resolveVisitorSegment(request, opts) {
27
+ const params = request.nextUrl.searchParams;
28
+ const medium = (params.get("utm_medium") || "").toLowerCase().trim();
29
+ const campaign = params.get("utm_campaign");
30
+ let source = "direct";
31
+ if (PAID_MEDIUMS.includes(medium)) {
32
+ source = "paid";
33
+ } else if (SOCIAL_MEDIUMS.includes(medium)) {
34
+ source = "social";
35
+ } else if (medium === "organic") {
36
+ source = "organic";
37
+ } else if (medium === "referral") {
38
+ source = "referral";
39
+ } else if (params.get("utm_source") || params.get("gclid") || params.get("fbclid")) {
40
+ if (params.get("gclid")) source = "paid";
41
+ else if (params.get("fbclid")) source = "social";
42
+ else source = "referral";
43
+ } else {
44
+ const ref = request.headers.get("referer") || "";
45
+ if (ref) {
46
+ try {
47
+ const host = new URL(ref).host.toLowerCase();
48
+ const selfHost = request.nextUrl.host.toLowerCase();
49
+ if (host && host !== selfHost) {
50
+ if (SEARCH_HOSTS.some((h) => host.includes(h))) source = "organic";
51
+ else if (SOCIAL_HOSTS.some((h) => host.includes(h))) source = "social";
52
+ else source = "referral";
53
+ }
54
+ } catch {
55
+ }
56
+ }
57
+ }
58
+ return {
59
+ visitorType: opts.hadVisitorCookie ? "returning" : "new",
60
+ source,
61
+ campaign: campaign || null
62
+ };
63
+ }
64
+ function encodeSegment(seg) {
65
+ const campaign = (seg.campaign || "").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 40);
66
+ return `${seg.visitorType}|${seg.source}|${campaign}`;
67
+ }
68
+ function decodeSegment(value) {
69
+ if (!value) return null;
70
+ const [visitorType, source, campaign] = value.split("|");
71
+ if (visitorType !== "new" && visitorType !== "returning") return null;
72
+ return {
73
+ visitorType,
74
+ source: ["paid", "organic", "social", "referral", "direct"].includes(source) ? source : "direct",
75
+ campaign: campaign || null
76
+ };
77
+ }
78
+
79
+ // src/middleware/index.ts
10
80
  function createMiddleware(config) {
11
81
  const securityHeaders = chunk3MYZS6PD_js.buildSecurityHeaders(
12
82
  config?.securityHeaders
@@ -42,12 +112,44 @@ function createMiddleware(config) {
42
112
  applyHeaders(response, discoveryHeaders);
43
113
  }
44
114
  }
115
+ if (config?.identity) {
116
+ const idCfg = typeof config.identity === "object" ? config.identity : {};
117
+ const hadVisitorCookie = !!request.cookies.get("_sk_vid")?.value;
118
+ if (!hadVisitorCookie) {
119
+ const maxAgeDays = idCfg.cookieMaxAgeDays ?? 730;
120
+ response.cookies.set("_sk_vid", generateVisitorId(), {
121
+ maxAge: maxAgeDays * 24 * 60 * 60,
122
+ path: "/",
123
+ sameSite: "lax",
124
+ secure: true
125
+ });
126
+ }
127
+ if (idCfg.segment !== false) {
128
+ const segment = encodeSegment(
129
+ resolveVisitorSegment(request, { hadVisitorCookie })
130
+ );
131
+ response.cookies.set("_sk_seg", segment, {
132
+ // Session-ish: a fresh campaign / source on a later visit re-segments.
133
+ maxAge: 30 * 60,
134
+ path: "/",
135
+ sameSite: "lax",
136
+ secure: true
137
+ });
138
+ }
139
+ }
45
140
  if (config?.after) {
46
141
  response = await config.after(request, response);
47
142
  }
48
143
  return response;
49
144
  };
50
145
  }
146
+ function generateVisitorId() {
147
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
148
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
149
+ const r = Math.random() * 16 | 0;
150
+ return (c === "x" ? r : r & 3 | 8).toString(16);
151
+ });
152
+ }
51
153
  function applyHeaders(response, headers) {
52
154
  for (const [key, value] of Object.entries(headers)) {
53
155
  response.headers.set(key, value);
@@ -62,6 +164,9 @@ var createProxy = createMiddleware;
62
164
 
63
165
  exports.createMiddleware = createMiddleware;
64
166
  exports.createProxy = createProxy;
167
+ exports.decodeSegment = decodeSegment;
168
+ exports.encodeSegment = encodeSegment;
169
+ exports.resolveVisitorSegment = resolveVisitorSegment;
65
170
  exports.siteKitMatcher = siteKitMatcher;
66
171
  //# sourceMappingURL=index.js.map
67
172
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middleware/index.ts"],"names":["buildSecurityHeaders","buildAiDiscoveryHeaders","handleManagedRedirects","NextResponse"],"mappings":";;;;;;;;;AAiFO,SAAS,iBAAiB,MAAA,EAAkC;AACjE,EAAA,MAAM,eAAA,GAAkBA,qCAAA;AAAA,IACtB,MAAA,EAAQ;AAAA,GACV;AAGA,EAAA,MAAM,gBAAA,GACJ,MAAA,EAAQ,aAAA,GACJC,wCAAA,CAAwB;AAAA,IACtB,OAAA,EAAS,OAAO,aAAA,CAAc,OAAA;AAAA,IAC9B,QAAA,EAAU,OAAO,aAAA,CAAc;AAAA,GAChC,IACD,EAAC;AAEP,EAAA,OAAO,eAAe,MACpB,OAAA,EACuB;AAEvB,IAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,MAAA,MAAM,aAAA,GAAgB,MAAM,MAAA,CAAO,MAAA,CAAO,OAAO,CAAA;AACjD,MAAA,IAAI,aAAA,EAAe;AAEjB,QAAA,YAAA,CAAa,eAAe,eAAe,CAAA;AAC3C,QAAA,OAAO,aAAA;AAAA,MACT;AAAA,IACF;AAGA,IAAA,IAAI,MAAA,EAAQ,cAAc,KAAA,EAAO;AAC/B,MAAA,MAAM,iBACJ,OAAO,MAAA,EAAQ,cAAc,QAAA,GACzB,MAAA,CAAO,YACP,EAAC;AAEP,MAAA,MAAM,WAAW,MAAMC,uCAAA;AAAA,QACrB,OAAA;AAAA,QACA;AAAA,OACF;AACA,MAAA,IAAI,QAAA,EAAU;AAEZ,QAAA,YAAA,CAAa,UAAU,eAAe,CAAA;AACtC,QAAA,OAAO,QAAA;AAAA,MACT;AAAA,IACF;AAGA,IAAA,IAAI,QAAA,GAAWC,oBAAa,IAAA,EAAK;AAGjC,IAAA,YAAA,CAAa,UAAU,eAAe,CAAA;AAGtC,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,gBAAgB,CAAA,CAAE,SAAS,CAAA,EAAG;AAC5C,MAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA,IAAK,EAAA;AAChD,MAAA,IAAI,OAAO,QAAA,CAAS,WAAW,KAAK,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG;AAC1D,QAAA,YAAA,CAAa,UAAU,gBAAgB,CAAA;AAAA,MACzC;AAAA,IACF;AAGA,IAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,MAAA,QAAA,GAAW,MAAM,MAAA,CAAO,KAAA,CAAM,OAAA,EAAS,QAAQ,CAAA;AAAA,IACjD;AAEA,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AACF;AAEA,SAAS,YAAA,CACP,UACA,OAAA,EACM;AACN,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,IAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,EACjC;AACF;AAMO,IAAM,cAAA,GAAiB;AAAA,EAC5B,OAAA,EAAS;AAAA,IACP;AAAA;AAEJ;AAGO,IAAM,WAAA,GAAc","file":"index.js","sourcesContent":["/**\n * @sonordev/site-kit/middleware\n *\n * Composable Next.js middleware factory. Handles:\n * - Portal-managed redirects (via existing handleManagedRedirects)\n * - Security headers (X-Frame-Options, CSP, etc.)\n * - Optional before/after hooks for custom logic\n *\n * @example\n * ```ts\n * // middleware.ts — entire file\n * import { createMiddleware, siteKitMatcher } from '@sonordev/site-kit/middleware'\n *\n * export default createMiddleware()\n * export const config = siteKitMatcher\n * ```\n *\n * @example With custom hooks:\n * ```ts\n * export default createMiddleware({\n * securityHeaders: { frameOptions: 'SAMEORIGIN' },\n * before: (req) => {\n * // Custom auth check, etc.\n * },\n * })\n * ```\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport {\n handleManagedRedirects,\n type RedirectConfig,\n} from '../redirects'\nimport {\n buildSecurityHeaders,\n type SecurityHeadersConfig,\n} from './securityHeaders'\nimport { buildAiDiscoveryHeaders } from '../llms/discovery-headers'\n\nexport type { SecurityHeadersConfig } from './securityHeaders'\n\nexport interface LlmsDiscoveryConfig {\n /** Canonical site origin, e.g. `https://example.com` */\n siteUrl: string\n /** Path to llms.txt (default `/llms.txt`) */\n llmsPath?: string\n}\n\nexport interface SiteKitMiddlewareConfig {\n /** Enable Portal-managed redirects. Default: true */\n redirects?: boolean | RedirectConfig\n /** Enable security headers. Default: true */\n securityHeaders?: boolean | SecurityHeadersConfig\n /**\n * Opt-in: inject `Link: <...>; rel=\"describedby\"` header on HTML responses\n * pointing to /llms.txt for AI crawler discovery (RFC 8288).\n * Preferred over visible footer links.\n */\n llmsDiscovery?: LlmsDiscoveryConfig | false\n /**\n * Hook that runs BEFORE redirect check.\n * Return a NextResponse to short-circuit, or undefined to continue.\n */\n before?: (\n request: NextRequest,\n ) => NextResponse | undefined | Promise<NextResponse | undefined>\n /**\n * Hook that runs AFTER all processing.\n * Receives the response that will be sent — mutate headers, etc.\n */\n after?: (\n request: NextRequest,\n response: NextResponse,\n ) => NextResponse | Promise<NextResponse>\n}\n\n/**\n * Create a fully-featured Next.js middleware function.\n *\n * With zero config this handles redirects + security headers automatically.\n */\nexport function createMiddleware(config?: SiteKitMiddlewareConfig) {\n const securityHeaders = buildSecurityHeaders(\n config?.securityHeaders,\n )\n\n // Pre-compute discovery headers (static per middleware instance)\n const discoveryHeaders: Record<string, string> =\n config?.llmsDiscovery\n ? buildAiDiscoveryHeaders({\n siteUrl: config.llmsDiscovery.siteUrl,\n llmsPath: config.llmsDiscovery.llmsPath,\n })\n : {}\n\n return async function proxy(\n request: NextRequest,\n ): Promise<NextResponse> {\n // 1. Run user's 'before' hook\n if (config?.before) {\n const earlyResponse = await config.before(request)\n if (earlyResponse) {\n // Apply security headers even to early responses\n applyHeaders(earlyResponse, securityHeaders)\n return earlyResponse\n }\n }\n\n // 2. Check Portal-managed redirects (unless disabled)\n if (config?.redirects !== false) {\n const redirectConfig: RedirectConfig =\n typeof config?.redirects === 'object'\n ? config.redirects\n : {}\n\n const redirect = await handleManagedRedirects(\n request,\n redirectConfig,\n )\n if (redirect) {\n // Redirects get security headers too\n applyHeaders(redirect, securityHeaders)\n return redirect\n }\n }\n\n // 3. Build the pass-through response\n let response = NextResponse.next()\n\n // 4. Apply security headers\n applyHeaders(response, securityHeaders)\n\n // 5. Apply AI discovery Link header (only on HTML-like requests, not static assets)\n if (Object.keys(discoveryHeaders).length > 0) {\n const accept = request.headers.get('accept') || ''\n if (accept.includes('text/html') || accept.includes('*/*')) {\n applyHeaders(response, discoveryHeaders)\n }\n }\n\n // 6. Run user's 'after' hook\n if (config?.after) {\n response = await config.after(request, response)\n }\n\n return response\n }\n}\n\nfunction applyHeaders(\n response: NextResponse,\n headers: Record<string, string>,\n): void {\n for (const [key, value] of Object.entries(headers)) {\n response.headers.set(key, value)\n }\n}\n\n/**\n * Standard matcher config that excludes static assets and images.\n * Export alongside createProxy/createMiddleware in your proxy.ts.\n */\nexport const siteKitMatcher = {\n matcher: [\n '/((?!_next/static|_next/image|favicon\\\\.ico|.*\\\\.(?:ico|png|jpg|jpeg|gif|webp|svg|woff2?)$).*)',\n ],\n}\n\n/** Next.js 16+ alias — same as createMiddleware, named for the proxy convention. */\nexport const createProxy = createMiddleware\n"]}
1
+ {"version":3,"sources":["../../src/middleware/segment.ts","../../src/middleware/index.ts"],"names":["buildSecurityHeaders","buildAiDiscoveryHeaders","handleManagedRedirects","NextResponse"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @sonordev/site-kit/middleware — visitor segment resolution (the \"one edge pass\").\n *\n * Pure, edge-runtime-safe helpers that classify a request into a coarse visitor\n * segment (new-vs-returning + marketing source + campaign) from the URL's UTM\n * params and the referrer. The middleware writes the result to the `_sk_seg`\n * cookie so server-rendered slots / personalization can vary by segment WITHOUT\n * each one re-parsing the request. Resolved once, at the edge.\n *\n * Kept side-effect-free (no NextResponse, no cookie writes) so it is trivially\n * unit-testable and reusable.\n */\nimport type { NextRequest } from 'next/server'\n\nexport type SegmentSource = 'paid' | 'organic' | 'social' | 'referral' | 'direct'\n\nexport interface VisitorSegment {\n /** `new` on the first visit (no prior `_sk_vid` cookie), else `returning`. */\n visitorType: 'new' | 'returning'\n /** Marketing source bucket derived from UTM medium/source, then referrer. */\n source: SegmentSource\n /** `utm_campaign`, if present (sanitized when encoded). */\n campaign: string | null\n}\n\nconst PAID_MEDIUMS = ['cpc', 'ppc', 'paid', 'paidsearch', 'paid-search', 'display', 'cpm', 'banner']\nconst SOCIAL_MEDIUMS = ['social', 'social-media', 'social_media', 'sm']\nconst SOCIAL_HOSTS = [\n 'facebook.',\n 'instagram.',\n 'linkedin.',\n 't.co',\n 'twitter.',\n 'x.com',\n 'pinterest.',\n 'tiktok.',\n 'youtube.',\n 'reddit.',\n]\nconst SEARCH_HOSTS = ['google.', 'bing.', 'duckduckgo.', 'yahoo.', 'ecosia.', 'brave.', 'baidu.', 'yandex.']\n\n/**\n * Classify `request` into a {@link VisitorSegment}. UTM params win over the\n * referrer; a same-host referrer is ignored (internal navigation, not a source).\n */\nexport function resolveVisitorSegment(\n request: NextRequest,\n opts: { hadVisitorCookie: boolean },\n): VisitorSegment {\n const params = request.nextUrl.searchParams\n const medium = (params.get('utm_medium') || '').toLowerCase().trim()\n const campaign = params.get('utm_campaign')\n\n let source: SegmentSource = 'direct'\n\n if (PAID_MEDIUMS.includes(medium)) {\n source = 'paid'\n } else if (SOCIAL_MEDIUMS.includes(medium)) {\n source = 'social'\n } else if (medium === 'organic') {\n source = 'organic'\n } else if (medium === 'referral') {\n source = 'referral'\n } else if (params.get('utm_source') || params.get('gclid') || params.get('fbclid')) {\n // A tagged link with a non-standard medium: gclid => paid Google, fbclid => social.\n if (params.get('gclid')) source = 'paid'\n else if (params.get('fbclid')) source = 'social'\n else source = 'referral'\n } else {\n // No campaign tagging — fall back to the referrer host.\n const ref = request.headers.get('referer') || ''\n if (ref) {\n try {\n const host = new URL(ref).host.toLowerCase()\n const selfHost = request.nextUrl.host.toLowerCase()\n if (host && host !== selfHost) {\n if (SEARCH_HOSTS.some((h) => host.includes(h))) source = 'organic'\n else if (SOCIAL_HOSTS.some((h) => host.includes(h))) source = 'social'\n else source = 'referral'\n }\n } catch {\n /* malformed referer — stays 'direct' */\n }\n }\n }\n\n return {\n visitorType: opts.hadVisitorCookie ? 'returning' : 'new',\n source,\n campaign: campaign || null,\n }\n}\n\n/**\n * Compact, cookie-safe encoding: `\"<visitorType>|<source>|<campaign>\"`, e.g.\n * `\"new|paid|spring_sale\"`. Campaign is sanitized to `[A-Za-z0-9_-]` and capped.\n * Decode with {@link decodeSegment}.\n */\nexport function encodeSegment(seg: VisitorSegment): string {\n const campaign = (seg.campaign || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 40)\n return `${seg.visitorType}|${seg.source}|${campaign}`\n}\n\n/** Parse a `_sk_seg` cookie value produced by {@link encodeSegment}. */\nexport function decodeSegment(value: string | undefined | null): VisitorSegment | null {\n if (!value) return null\n const [visitorType, source, campaign] = value.split('|')\n if (visitorType !== 'new' && visitorType !== 'returning') return null\n return {\n visitorType,\n source: (['paid', 'organic', 'social', 'referral', 'direct'].includes(source) ? source : 'direct') as SegmentSource,\n campaign: campaign || null,\n }\n}\n","/**\n * @sonordev/site-kit/middleware\n *\n * Composable Next.js middleware factory. Handles:\n * - Portal-managed redirects (via existing handleManagedRedirects)\n * - Security headers (X-Frame-Options, CSP, etc.)\n * - Optional before/after hooks for custom logic\n *\n * @example\n * ```ts\n * // middleware.ts — entire file\n * import { createMiddleware, siteKitMatcher } from '@sonordev/site-kit/middleware'\n *\n * export default createMiddleware()\n * export const config = siteKitMatcher\n * ```\n *\n * @example With custom hooks:\n * ```ts\n * export default createMiddleware({\n * securityHeaders: { frameOptions: 'SAMEORIGIN' },\n * before: (req) => {\n * // Custom auth check, etc.\n * },\n * })\n * ```\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport {\n handleManagedRedirects,\n type RedirectConfig,\n} from '../redirects'\nimport {\n buildSecurityHeaders,\n type SecurityHeadersConfig,\n} from './securityHeaders'\nimport { buildAiDiscoveryHeaders } from '../llms/discovery-headers'\nimport { resolveVisitorSegment, encodeSegment } from './segment'\n\nexport type { SecurityHeadersConfig } from './securityHeaders'\nexport {\n resolveVisitorSegment,\n encodeSegment,\n decodeSegment,\n type VisitorSegment,\n type SegmentSource,\n} from './segment'\n\nexport interface IdentityConfig {\n /**\n * Also resolve + persist a coarse visitor segment (new-vs-returning + source\n * + campaign) into the `_sk_seg` cookie, for segment-aware slots /\n * personalization. Default: true (when identity is enabled).\n */\n segment?: boolean\n /** Days the `_sk_vid` visitor cookie persists. Default: 730 (~2 years). */\n cookieMaxAgeDays?: number\n}\n\nexport interface LlmsDiscoveryConfig {\n /** Canonical site origin, e.g. `https://example.com` */\n siteUrl: string\n /** Path to llms.txt (default `/llms.txt`) */\n llmsPath?: string\n}\n\nexport interface SiteKitMiddlewareConfig {\n /** Enable Portal-managed redirects. Default: true */\n redirects?: boolean | RedirectConfig\n /** Enable security headers. Default: true */\n securityHeaders?: boolean | SecurityHeadersConfig\n /**\n * Opt-in: inject `Link: <...>; rel=\"describedby\"` header on HTML responses\n * pointing to /llms.txt for AI crawler discovery (RFC 8288).\n * Preferred over visible footer links.\n */\n llmsDiscovery?: LlmsDiscoveryConfig | false\n /**\n * Opt-in EDGE IDENTITY pass. When enabled, the middleware ensures a\n * first-party `_sk_vid` visitor cookie exists (server-authoritative id) and\n * resolves the visitor segment into `_sk_seg`, so server-rendered slots and\n * personalization can read identity + segment without re-parsing the request.\n * Purely additive — it only writes cookies, never changes the rendered HTML,\n * so it is LCP/static-safe. Default: OFF (sites opt in for 3.0 personalization).\n *\n * NOTE: to make these the SAME id the client uses, the shared identity helper\n * must read `_sk_vid` cookie-first (a follow-on change validated with a live\n * preview); until then the cookie is the server-side mirror.\n */\n identity?: boolean | IdentityConfig\n /**\n * Hook that runs BEFORE redirect check.\n * Return a NextResponse to short-circuit, or undefined to continue.\n */\n before?: (\n request: NextRequest,\n ) => NextResponse | undefined | Promise<NextResponse | undefined>\n /**\n * Hook that runs AFTER all processing.\n * Receives the response that will be sent — mutate headers, etc.\n */\n after?: (\n request: NextRequest,\n response: NextResponse,\n ) => NextResponse | Promise<NextResponse>\n}\n\n/**\n * Create a fully-featured Next.js middleware function.\n *\n * With zero config this handles redirects + security headers automatically.\n */\nexport function createMiddleware(config?: SiteKitMiddlewareConfig) {\n const securityHeaders = buildSecurityHeaders(\n config?.securityHeaders,\n )\n\n // Pre-compute discovery headers (static per middleware instance)\n const discoveryHeaders: Record<string, string> =\n config?.llmsDiscovery\n ? buildAiDiscoveryHeaders({\n siteUrl: config.llmsDiscovery.siteUrl,\n llmsPath: config.llmsDiscovery.llmsPath,\n })\n : {}\n\n return async function proxy(\n request: NextRequest,\n ): Promise<NextResponse> {\n // 1. Run user's 'before' hook\n if (config?.before) {\n const earlyResponse = await config.before(request)\n if (earlyResponse) {\n // Apply security headers even to early responses\n applyHeaders(earlyResponse, securityHeaders)\n return earlyResponse\n }\n }\n\n // 2. Check Portal-managed redirects (unless disabled)\n if (config?.redirects !== false) {\n const redirectConfig: RedirectConfig =\n typeof config?.redirects === 'object'\n ? config.redirects\n : {}\n\n const redirect = await handleManagedRedirects(\n request,\n redirectConfig,\n )\n if (redirect) {\n // Redirects get security headers too\n applyHeaders(redirect, securityHeaders)\n return redirect\n }\n }\n\n // 3. Build the pass-through response\n let response = NextResponse.next()\n\n // 4. Apply security headers\n applyHeaders(response, securityHeaders)\n\n // 5. Apply AI discovery Link header (only on HTML-like requests, not static assets)\n if (Object.keys(discoveryHeaders).length > 0) {\n const accept = request.headers.get('accept') || ''\n if (accept.includes('text/html') || accept.includes('*/*')) {\n applyHeaders(response, discoveryHeaders)\n }\n }\n\n // 5b. Edge identity pass (opt-in). Ensure a first-party `_sk_vid` visitor\n // cookie exists and resolve the visitor segment into `_sk_seg`. Additive:\n // only writes cookies, never changes the rendered HTML (LCP/static-safe).\n if (config?.identity) {\n const idCfg: IdentityConfig =\n typeof config.identity === 'object' ? config.identity : {}\n const hadVisitorCookie = !!request.cookies.get('_sk_vid')?.value\n if (!hadVisitorCookie) {\n const maxAgeDays = idCfg.cookieMaxAgeDays ?? 730\n response.cookies.set('_sk_vid', generateVisitorId(), {\n maxAge: maxAgeDays * 24 * 60 * 60,\n path: '/',\n sameSite: 'lax',\n secure: true,\n })\n }\n if (idCfg.segment !== false) {\n const segment = encodeSegment(\n resolveVisitorSegment(request, { hadVisitorCookie }),\n )\n response.cookies.set('_sk_seg', segment, {\n // Session-ish: a fresh campaign / source on a later visit re-segments.\n maxAge: 30 * 60,\n path: '/',\n sameSite: 'lax',\n secure: true,\n })\n }\n }\n\n // 6. Run user's 'after' hook\n if (config?.after) {\n response = await config.after(request, response)\n }\n\n return response\n }\n}\n\n/**\n * Generate a visitor id at the edge. Prefers Web Crypto (available in the\n * Next.js edge runtime); falls back to a v4-shaped string elsewhere.\n */\nfunction generateVisitorId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)\n })\n}\n\nfunction applyHeaders(\n response: NextResponse,\n headers: Record<string, string>,\n): void {\n for (const [key, value] of Object.entries(headers)) {\n response.headers.set(key, value)\n }\n}\n\n/**\n * Standard matcher config that excludes static assets and images.\n * Export alongside createProxy/createMiddleware in your proxy.ts.\n */\nexport const siteKitMatcher = {\n matcher: [\n '/((?!_next/static|_next/image|favicon\\\\.ico|.*\\\\.(?:ico|png|jpg|jpeg|gif|webp|svg|woff2?)$).*)',\n ],\n}\n\n/** Next.js 16+ alias — same as createMiddleware, named for the proxy convention. */\nexport const createProxy = createMiddleware\n"]}
@@ -5,6 +5,76 @@ import '../chunk-B22WSN4L.mjs';
5
5
  import '../chunk-4XPGGLVP.mjs';
6
6
  import { NextResponse } from 'next/server';
7
7
 
8
+ // src/middleware/segment.ts
9
+ var PAID_MEDIUMS = ["cpc", "ppc", "paid", "paidsearch", "paid-search", "display", "cpm", "banner"];
10
+ var SOCIAL_MEDIUMS = ["social", "social-media", "social_media", "sm"];
11
+ var SOCIAL_HOSTS = [
12
+ "facebook.",
13
+ "instagram.",
14
+ "linkedin.",
15
+ "t.co",
16
+ "twitter.",
17
+ "x.com",
18
+ "pinterest.",
19
+ "tiktok.",
20
+ "youtube.",
21
+ "reddit."
22
+ ];
23
+ var SEARCH_HOSTS = ["google.", "bing.", "duckduckgo.", "yahoo.", "ecosia.", "brave.", "baidu.", "yandex."];
24
+ function resolveVisitorSegment(request, opts) {
25
+ const params = request.nextUrl.searchParams;
26
+ const medium = (params.get("utm_medium") || "").toLowerCase().trim();
27
+ const campaign = params.get("utm_campaign");
28
+ let source = "direct";
29
+ if (PAID_MEDIUMS.includes(medium)) {
30
+ source = "paid";
31
+ } else if (SOCIAL_MEDIUMS.includes(medium)) {
32
+ source = "social";
33
+ } else if (medium === "organic") {
34
+ source = "organic";
35
+ } else if (medium === "referral") {
36
+ source = "referral";
37
+ } else if (params.get("utm_source") || params.get("gclid") || params.get("fbclid")) {
38
+ if (params.get("gclid")) source = "paid";
39
+ else if (params.get("fbclid")) source = "social";
40
+ else source = "referral";
41
+ } else {
42
+ const ref = request.headers.get("referer") || "";
43
+ if (ref) {
44
+ try {
45
+ const host = new URL(ref).host.toLowerCase();
46
+ const selfHost = request.nextUrl.host.toLowerCase();
47
+ if (host && host !== selfHost) {
48
+ if (SEARCH_HOSTS.some((h) => host.includes(h))) source = "organic";
49
+ else if (SOCIAL_HOSTS.some((h) => host.includes(h))) source = "social";
50
+ else source = "referral";
51
+ }
52
+ } catch {
53
+ }
54
+ }
55
+ }
56
+ return {
57
+ visitorType: opts.hadVisitorCookie ? "returning" : "new",
58
+ source,
59
+ campaign: campaign || null
60
+ };
61
+ }
62
+ function encodeSegment(seg) {
63
+ const campaign = (seg.campaign || "").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 40);
64
+ return `${seg.visitorType}|${seg.source}|${campaign}`;
65
+ }
66
+ function decodeSegment(value) {
67
+ if (!value) return null;
68
+ const [visitorType, source, campaign] = value.split("|");
69
+ if (visitorType !== "new" && visitorType !== "returning") return null;
70
+ return {
71
+ visitorType,
72
+ source: ["paid", "organic", "social", "referral", "direct"].includes(source) ? source : "direct",
73
+ campaign: campaign || null
74
+ };
75
+ }
76
+
77
+ // src/middleware/index.ts
8
78
  function createMiddleware(config) {
9
79
  const securityHeaders = buildSecurityHeaders(
10
80
  config?.securityHeaders
@@ -40,12 +110,44 @@ function createMiddleware(config) {
40
110
  applyHeaders(response, discoveryHeaders);
41
111
  }
42
112
  }
113
+ if (config?.identity) {
114
+ const idCfg = typeof config.identity === "object" ? config.identity : {};
115
+ const hadVisitorCookie = !!request.cookies.get("_sk_vid")?.value;
116
+ if (!hadVisitorCookie) {
117
+ const maxAgeDays = idCfg.cookieMaxAgeDays ?? 730;
118
+ response.cookies.set("_sk_vid", generateVisitorId(), {
119
+ maxAge: maxAgeDays * 24 * 60 * 60,
120
+ path: "/",
121
+ sameSite: "lax",
122
+ secure: true
123
+ });
124
+ }
125
+ if (idCfg.segment !== false) {
126
+ const segment = encodeSegment(
127
+ resolveVisitorSegment(request, { hadVisitorCookie })
128
+ );
129
+ response.cookies.set("_sk_seg", segment, {
130
+ // Session-ish: a fresh campaign / source on a later visit re-segments.
131
+ maxAge: 30 * 60,
132
+ path: "/",
133
+ sameSite: "lax",
134
+ secure: true
135
+ });
136
+ }
137
+ }
43
138
  if (config?.after) {
44
139
  response = await config.after(request, response);
45
140
  }
46
141
  return response;
47
142
  };
48
143
  }
144
+ function generateVisitorId() {
145
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
146
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
147
+ const r = Math.random() * 16 | 0;
148
+ return (c === "x" ? r : r & 3 | 8).toString(16);
149
+ });
150
+ }
49
151
  function applyHeaders(response, headers) {
50
152
  for (const [key, value] of Object.entries(headers)) {
51
153
  response.headers.set(key, value);
@@ -58,6 +160,6 @@ var siteKitMatcher = {
58
160
  };
59
161
  var createProxy = createMiddleware;
60
162
 
61
- export { createMiddleware, createProxy, siteKitMatcher };
163
+ export { createMiddleware, createProxy, decodeSegment, encodeSegment, resolveVisitorSegment, siteKitMatcher };
62
164
  //# sourceMappingURL=index.mjs.map
63
165
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middleware/index.ts"],"names":[],"mappings":";;;;;;;AAiFO,SAAS,iBAAiB,MAAA,EAAkC;AACjE,EAAA,MAAM,eAAA,GAAkB,oBAAA;AAAA,IACtB,MAAA,EAAQ;AAAA,GACV;AAGA,EAAA,MAAM,gBAAA,GACJ,MAAA,EAAQ,aAAA,GACJ,uBAAA,CAAwB;AAAA,IACtB,OAAA,EAAS,OAAO,aAAA,CAAc,OAAA;AAAA,IAC9B,QAAA,EAAU,OAAO,aAAA,CAAc;AAAA,GAChC,IACD,EAAC;AAEP,EAAA,OAAO,eAAe,MACpB,OAAA,EACuB;AAEvB,IAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,MAAA,MAAM,aAAA,GAAgB,MAAM,MAAA,CAAO,MAAA,CAAO,OAAO,CAAA;AACjD,MAAA,IAAI,aAAA,EAAe;AAEjB,QAAA,YAAA,CAAa,eAAe,eAAe,CAAA;AAC3C,QAAA,OAAO,aAAA;AAAA,MACT;AAAA,IACF;AAGA,IAAA,IAAI,MAAA,EAAQ,cAAc,KAAA,EAAO;AAC/B,MAAA,MAAM,iBACJ,OAAO,MAAA,EAAQ,cAAc,QAAA,GACzB,MAAA,CAAO,YACP,EAAC;AAEP,MAAA,MAAM,WAAW,MAAM,sBAAA;AAAA,QACrB,OAAA;AAAA,QACA;AAAA,OACF;AACA,MAAA,IAAI,QAAA,EAAU;AAEZ,QAAA,YAAA,CAAa,UAAU,eAAe,CAAA;AACtC,QAAA,OAAO,QAAA;AAAA,MACT;AAAA,IACF;AAGA,IAAA,IAAI,QAAA,GAAW,aAAa,IAAA,EAAK;AAGjC,IAAA,YAAA,CAAa,UAAU,eAAe,CAAA;AAGtC,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,gBAAgB,CAAA,CAAE,SAAS,CAAA,EAAG;AAC5C,MAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA,IAAK,EAAA;AAChD,MAAA,IAAI,OAAO,QAAA,CAAS,WAAW,KAAK,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG;AAC1D,QAAA,YAAA,CAAa,UAAU,gBAAgB,CAAA;AAAA,MACzC;AAAA,IACF;AAGA,IAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,MAAA,QAAA,GAAW,MAAM,MAAA,CAAO,KAAA,CAAM,OAAA,EAAS,QAAQ,CAAA;AAAA,IACjD;AAEA,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AACF;AAEA,SAAS,YAAA,CACP,UACA,OAAA,EACM;AACN,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,IAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,EACjC;AACF;AAMO,IAAM,cAAA,GAAiB;AAAA,EAC5B,OAAA,EAAS;AAAA,IACP;AAAA;AAEJ;AAGO,IAAM,WAAA,GAAc","file":"index.mjs","sourcesContent":["/**\n * @sonordev/site-kit/middleware\n *\n * Composable Next.js middleware factory. Handles:\n * - Portal-managed redirects (via existing handleManagedRedirects)\n * - Security headers (X-Frame-Options, CSP, etc.)\n * - Optional before/after hooks for custom logic\n *\n * @example\n * ```ts\n * // middleware.ts — entire file\n * import { createMiddleware, siteKitMatcher } from '@sonordev/site-kit/middleware'\n *\n * export default createMiddleware()\n * export const config = siteKitMatcher\n * ```\n *\n * @example With custom hooks:\n * ```ts\n * export default createMiddleware({\n * securityHeaders: { frameOptions: 'SAMEORIGIN' },\n * before: (req) => {\n * // Custom auth check, etc.\n * },\n * })\n * ```\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport {\n handleManagedRedirects,\n type RedirectConfig,\n} from '../redirects'\nimport {\n buildSecurityHeaders,\n type SecurityHeadersConfig,\n} from './securityHeaders'\nimport { buildAiDiscoveryHeaders } from '../llms/discovery-headers'\n\nexport type { SecurityHeadersConfig } from './securityHeaders'\n\nexport interface LlmsDiscoveryConfig {\n /** Canonical site origin, e.g. `https://example.com` */\n siteUrl: string\n /** Path to llms.txt (default `/llms.txt`) */\n llmsPath?: string\n}\n\nexport interface SiteKitMiddlewareConfig {\n /** Enable Portal-managed redirects. Default: true */\n redirects?: boolean | RedirectConfig\n /** Enable security headers. Default: true */\n securityHeaders?: boolean | SecurityHeadersConfig\n /**\n * Opt-in: inject `Link: <...>; rel=\"describedby\"` header on HTML responses\n * pointing to /llms.txt for AI crawler discovery (RFC 8288).\n * Preferred over visible footer links.\n */\n llmsDiscovery?: LlmsDiscoveryConfig | false\n /**\n * Hook that runs BEFORE redirect check.\n * Return a NextResponse to short-circuit, or undefined to continue.\n */\n before?: (\n request: NextRequest,\n ) => NextResponse | undefined | Promise<NextResponse | undefined>\n /**\n * Hook that runs AFTER all processing.\n * Receives the response that will be sent — mutate headers, etc.\n */\n after?: (\n request: NextRequest,\n response: NextResponse,\n ) => NextResponse | Promise<NextResponse>\n}\n\n/**\n * Create a fully-featured Next.js middleware function.\n *\n * With zero config this handles redirects + security headers automatically.\n */\nexport function createMiddleware(config?: SiteKitMiddlewareConfig) {\n const securityHeaders = buildSecurityHeaders(\n config?.securityHeaders,\n )\n\n // Pre-compute discovery headers (static per middleware instance)\n const discoveryHeaders: Record<string, string> =\n config?.llmsDiscovery\n ? buildAiDiscoveryHeaders({\n siteUrl: config.llmsDiscovery.siteUrl,\n llmsPath: config.llmsDiscovery.llmsPath,\n })\n : {}\n\n return async function proxy(\n request: NextRequest,\n ): Promise<NextResponse> {\n // 1. Run user's 'before' hook\n if (config?.before) {\n const earlyResponse = await config.before(request)\n if (earlyResponse) {\n // Apply security headers even to early responses\n applyHeaders(earlyResponse, securityHeaders)\n return earlyResponse\n }\n }\n\n // 2. Check Portal-managed redirects (unless disabled)\n if (config?.redirects !== false) {\n const redirectConfig: RedirectConfig =\n typeof config?.redirects === 'object'\n ? config.redirects\n : {}\n\n const redirect = await handleManagedRedirects(\n request,\n redirectConfig,\n )\n if (redirect) {\n // Redirects get security headers too\n applyHeaders(redirect, securityHeaders)\n return redirect\n }\n }\n\n // 3. Build the pass-through response\n let response = NextResponse.next()\n\n // 4. Apply security headers\n applyHeaders(response, securityHeaders)\n\n // 5. Apply AI discovery Link header (only on HTML-like requests, not static assets)\n if (Object.keys(discoveryHeaders).length > 0) {\n const accept = request.headers.get('accept') || ''\n if (accept.includes('text/html') || accept.includes('*/*')) {\n applyHeaders(response, discoveryHeaders)\n }\n }\n\n // 6. Run user's 'after' hook\n if (config?.after) {\n response = await config.after(request, response)\n }\n\n return response\n }\n}\n\nfunction applyHeaders(\n response: NextResponse,\n headers: Record<string, string>,\n): void {\n for (const [key, value] of Object.entries(headers)) {\n response.headers.set(key, value)\n }\n}\n\n/**\n * Standard matcher config that excludes static assets and images.\n * Export alongside createProxy/createMiddleware in your proxy.ts.\n */\nexport const siteKitMatcher = {\n matcher: [\n '/((?!_next/static|_next/image|favicon\\\\.ico|.*\\\\.(?:ico|png|jpg|jpeg|gif|webp|svg|woff2?)$).*)',\n ],\n}\n\n/** Next.js 16+ alias — same as createMiddleware, named for the proxy convention. */\nexport const createProxy = createMiddleware\n"]}
1
+ {"version":3,"sources":["../../src/middleware/segment.ts","../../src/middleware/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @sonordev/site-kit/middleware — visitor segment resolution (the \"one edge pass\").\n *\n * Pure, edge-runtime-safe helpers that classify a request into a coarse visitor\n * segment (new-vs-returning + marketing source + campaign) from the URL's UTM\n * params and the referrer. The middleware writes the result to the `_sk_seg`\n * cookie so server-rendered slots / personalization can vary by segment WITHOUT\n * each one re-parsing the request. Resolved once, at the edge.\n *\n * Kept side-effect-free (no NextResponse, no cookie writes) so it is trivially\n * unit-testable and reusable.\n */\nimport type { NextRequest } from 'next/server'\n\nexport type SegmentSource = 'paid' | 'organic' | 'social' | 'referral' | 'direct'\n\nexport interface VisitorSegment {\n /** `new` on the first visit (no prior `_sk_vid` cookie), else `returning`. */\n visitorType: 'new' | 'returning'\n /** Marketing source bucket derived from UTM medium/source, then referrer. */\n source: SegmentSource\n /** `utm_campaign`, if present (sanitized when encoded). */\n campaign: string | null\n}\n\nconst PAID_MEDIUMS = ['cpc', 'ppc', 'paid', 'paidsearch', 'paid-search', 'display', 'cpm', 'banner']\nconst SOCIAL_MEDIUMS = ['social', 'social-media', 'social_media', 'sm']\nconst SOCIAL_HOSTS = [\n 'facebook.',\n 'instagram.',\n 'linkedin.',\n 't.co',\n 'twitter.',\n 'x.com',\n 'pinterest.',\n 'tiktok.',\n 'youtube.',\n 'reddit.',\n]\nconst SEARCH_HOSTS = ['google.', 'bing.', 'duckduckgo.', 'yahoo.', 'ecosia.', 'brave.', 'baidu.', 'yandex.']\n\n/**\n * Classify `request` into a {@link VisitorSegment}. UTM params win over the\n * referrer; a same-host referrer is ignored (internal navigation, not a source).\n */\nexport function resolveVisitorSegment(\n request: NextRequest,\n opts: { hadVisitorCookie: boolean },\n): VisitorSegment {\n const params = request.nextUrl.searchParams\n const medium = (params.get('utm_medium') || '').toLowerCase().trim()\n const campaign = params.get('utm_campaign')\n\n let source: SegmentSource = 'direct'\n\n if (PAID_MEDIUMS.includes(medium)) {\n source = 'paid'\n } else if (SOCIAL_MEDIUMS.includes(medium)) {\n source = 'social'\n } else if (medium === 'organic') {\n source = 'organic'\n } else if (medium === 'referral') {\n source = 'referral'\n } else if (params.get('utm_source') || params.get('gclid') || params.get('fbclid')) {\n // A tagged link with a non-standard medium: gclid => paid Google, fbclid => social.\n if (params.get('gclid')) source = 'paid'\n else if (params.get('fbclid')) source = 'social'\n else source = 'referral'\n } else {\n // No campaign tagging — fall back to the referrer host.\n const ref = request.headers.get('referer') || ''\n if (ref) {\n try {\n const host = new URL(ref).host.toLowerCase()\n const selfHost = request.nextUrl.host.toLowerCase()\n if (host && host !== selfHost) {\n if (SEARCH_HOSTS.some((h) => host.includes(h))) source = 'organic'\n else if (SOCIAL_HOSTS.some((h) => host.includes(h))) source = 'social'\n else source = 'referral'\n }\n } catch {\n /* malformed referer — stays 'direct' */\n }\n }\n }\n\n return {\n visitorType: opts.hadVisitorCookie ? 'returning' : 'new',\n source,\n campaign: campaign || null,\n }\n}\n\n/**\n * Compact, cookie-safe encoding: `\"<visitorType>|<source>|<campaign>\"`, e.g.\n * `\"new|paid|spring_sale\"`. Campaign is sanitized to `[A-Za-z0-9_-]` and capped.\n * Decode with {@link decodeSegment}.\n */\nexport function encodeSegment(seg: VisitorSegment): string {\n const campaign = (seg.campaign || '').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 40)\n return `${seg.visitorType}|${seg.source}|${campaign}`\n}\n\n/** Parse a `_sk_seg` cookie value produced by {@link encodeSegment}. */\nexport function decodeSegment(value: string | undefined | null): VisitorSegment | null {\n if (!value) return null\n const [visitorType, source, campaign] = value.split('|')\n if (visitorType !== 'new' && visitorType !== 'returning') return null\n return {\n visitorType,\n source: (['paid', 'organic', 'social', 'referral', 'direct'].includes(source) ? source : 'direct') as SegmentSource,\n campaign: campaign || null,\n }\n}\n","/**\n * @sonordev/site-kit/middleware\n *\n * Composable Next.js middleware factory. Handles:\n * - Portal-managed redirects (via existing handleManagedRedirects)\n * - Security headers (X-Frame-Options, CSP, etc.)\n * - Optional before/after hooks for custom logic\n *\n * @example\n * ```ts\n * // middleware.ts — entire file\n * import { createMiddleware, siteKitMatcher } from '@sonordev/site-kit/middleware'\n *\n * export default createMiddleware()\n * export const config = siteKitMatcher\n * ```\n *\n * @example With custom hooks:\n * ```ts\n * export default createMiddleware({\n * securityHeaders: { frameOptions: 'SAMEORIGIN' },\n * before: (req) => {\n * // Custom auth check, etc.\n * },\n * })\n * ```\n */\n\nimport { NextRequest, NextResponse } from 'next/server'\nimport {\n handleManagedRedirects,\n type RedirectConfig,\n} from '../redirects'\nimport {\n buildSecurityHeaders,\n type SecurityHeadersConfig,\n} from './securityHeaders'\nimport { buildAiDiscoveryHeaders } from '../llms/discovery-headers'\nimport { resolveVisitorSegment, encodeSegment } from './segment'\n\nexport type { SecurityHeadersConfig } from './securityHeaders'\nexport {\n resolveVisitorSegment,\n encodeSegment,\n decodeSegment,\n type VisitorSegment,\n type SegmentSource,\n} from './segment'\n\nexport interface IdentityConfig {\n /**\n * Also resolve + persist a coarse visitor segment (new-vs-returning + source\n * + campaign) into the `_sk_seg` cookie, for segment-aware slots /\n * personalization. Default: true (when identity is enabled).\n */\n segment?: boolean\n /** Days the `_sk_vid` visitor cookie persists. Default: 730 (~2 years). */\n cookieMaxAgeDays?: number\n}\n\nexport interface LlmsDiscoveryConfig {\n /** Canonical site origin, e.g. `https://example.com` */\n siteUrl: string\n /** Path to llms.txt (default `/llms.txt`) */\n llmsPath?: string\n}\n\nexport interface SiteKitMiddlewareConfig {\n /** Enable Portal-managed redirects. Default: true */\n redirects?: boolean | RedirectConfig\n /** Enable security headers. Default: true */\n securityHeaders?: boolean | SecurityHeadersConfig\n /**\n * Opt-in: inject `Link: <...>; rel=\"describedby\"` header on HTML responses\n * pointing to /llms.txt for AI crawler discovery (RFC 8288).\n * Preferred over visible footer links.\n */\n llmsDiscovery?: LlmsDiscoveryConfig | false\n /**\n * Opt-in EDGE IDENTITY pass. When enabled, the middleware ensures a\n * first-party `_sk_vid` visitor cookie exists (server-authoritative id) and\n * resolves the visitor segment into `_sk_seg`, so server-rendered slots and\n * personalization can read identity + segment without re-parsing the request.\n * Purely additive — it only writes cookies, never changes the rendered HTML,\n * so it is LCP/static-safe. Default: OFF (sites opt in for 3.0 personalization).\n *\n * NOTE: to make these the SAME id the client uses, the shared identity helper\n * must read `_sk_vid` cookie-first (a follow-on change validated with a live\n * preview); until then the cookie is the server-side mirror.\n */\n identity?: boolean | IdentityConfig\n /**\n * Hook that runs BEFORE redirect check.\n * Return a NextResponse to short-circuit, or undefined to continue.\n */\n before?: (\n request: NextRequest,\n ) => NextResponse | undefined | Promise<NextResponse | undefined>\n /**\n * Hook that runs AFTER all processing.\n * Receives the response that will be sent — mutate headers, etc.\n */\n after?: (\n request: NextRequest,\n response: NextResponse,\n ) => NextResponse | Promise<NextResponse>\n}\n\n/**\n * Create a fully-featured Next.js middleware function.\n *\n * With zero config this handles redirects + security headers automatically.\n */\nexport function createMiddleware(config?: SiteKitMiddlewareConfig) {\n const securityHeaders = buildSecurityHeaders(\n config?.securityHeaders,\n )\n\n // Pre-compute discovery headers (static per middleware instance)\n const discoveryHeaders: Record<string, string> =\n config?.llmsDiscovery\n ? buildAiDiscoveryHeaders({\n siteUrl: config.llmsDiscovery.siteUrl,\n llmsPath: config.llmsDiscovery.llmsPath,\n })\n : {}\n\n return async function proxy(\n request: NextRequest,\n ): Promise<NextResponse> {\n // 1. Run user's 'before' hook\n if (config?.before) {\n const earlyResponse = await config.before(request)\n if (earlyResponse) {\n // Apply security headers even to early responses\n applyHeaders(earlyResponse, securityHeaders)\n return earlyResponse\n }\n }\n\n // 2. Check Portal-managed redirects (unless disabled)\n if (config?.redirects !== false) {\n const redirectConfig: RedirectConfig =\n typeof config?.redirects === 'object'\n ? config.redirects\n : {}\n\n const redirect = await handleManagedRedirects(\n request,\n redirectConfig,\n )\n if (redirect) {\n // Redirects get security headers too\n applyHeaders(redirect, securityHeaders)\n return redirect\n }\n }\n\n // 3. Build the pass-through response\n let response = NextResponse.next()\n\n // 4. Apply security headers\n applyHeaders(response, securityHeaders)\n\n // 5. Apply AI discovery Link header (only on HTML-like requests, not static assets)\n if (Object.keys(discoveryHeaders).length > 0) {\n const accept = request.headers.get('accept') || ''\n if (accept.includes('text/html') || accept.includes('*/*')) {\n applyHeaders(response, discoveryHeaders)\n }\n }\n\n // 5b. Edge identity pass (opt-in). Ensure a first-party `_sk_vid` visitor\n // cookie exists and resolve the visitor segment into `_sk_seg`. Additive:\n // only writes cookies, never changes the rendered HTML (LCP/static-safe).\n if (config?.identity) {\n const idCfg: IdentityConfig =\n typeof config.identity === 'object' ? config.identity : {}\n const hadVisitorCookie = !!request.cookies.get('_sk_vid')?.value\n if (!hadVisitorCookie) {\n const maxAgeDays = idCfg.cookieMaxAgeDays ?? 730\n response.cookies.set('_sk_vid', generateVisitorId(), {\n maxAge: maxAgeDays * 24 * 60 * 60,\n path: '/',\n sameSite: 'lax',\n secure: true,\n })\n }\n if (idCfg.segment !== false) {\n const segment = encodeSegment(\n resolveVisitorSegment(request, { hadVisitorCookie }),\n )\n response.cookies.set('_sk_seg', segment, {\n // Session-ish: a fresh campaign / source on a later visit re-segments.\n maxAge: 30 * 60,\n path: '/',\n sameSite: 'lax',\n secure: true,\n })\n }\n }\n\n // 6. Run user's 'after' hook\n if (config?.after) {\n response = await config.after(request, response)\n }\n\n return response\n }\n}\n\n/**\n * Generate a visitor id at the edge. Prefers Web Crypto (available in the\n * Next.js edge runtime); falls back to a v4-shaped string elsewhere.\n */\nfunction generateVisitorId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)\n })\n}\n\nfunction applyHeaders(\n response: NextResponse,\n headers: Record<string, string>,\n): void {\n for (const [key, value] of Object.entries(headers)) {\n response.headers.set(key, value)\n }\n}\n\n/**\n * Standard matcher config that excludes static assets and images.\n * Export alongside createProxy/createMiddleware in your proxy.ts.\n */\nexport const siteKitMatcher = {\n matcher: [\n '/((?!_next/static|_next/image|favicon\\\\.ico|.*\\\\.(?:ico|png|jpg|jpeg|gif|webp|svg|woff2?)$).*)',\n ],\n}\n\n/** Next.js 16+ alias — same as createMiddleware, named for the proxy convention. */\nexport const createProxy = createMiddleware\n"]}