@sonordev/site-kit 3.3.0 → 3.4.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.
- package/agent-manifest.json +13 -1
- package/dist/AnalyticsProvider-ARAWVJPX.js +30 -0
- package/dist/{AnalyticsProvider-26VS3CQR.js.map → AnalyticsProvider-ARAWVJPX.js.map} +1 -1
- package/dist/AnalyticsProvider-HIGSPNRA.mjs +9 -0
- package/dist/{AnalyticsProvider-AMHG4DIE.mjs.map → AnalyticsProvider-HIGSPNRA.mjs.map} +1 -1
- package/dist/ChatWidget-47KAOFLG.mjs +9 -0
- package/dist/{ChatWidget-UKPEQEHB.mjs.map → ChatWidget-47KAOFLG.mjs.map} +1 -1
- package/dist/ChatWidget-BDES6WUR.js +18 -0
- package/dist/{ChatWidget-NIYNSMLU.js.map → ChatWidget-BDES6WUR.js.map} +1 -1
- package/dist/EngageWidget-HPZAI6SU.mjs +8 -0
- package/dist/{EngageWidget-S76ZMDEF.mjs.map → EngageWidget-HPZAI6SU.mjs.map} +1 -1
- package/dist/EngageWidget-X3NIWGMZ.js +17 -0
- package/dist/{EngageWidget-33QCFWQW.js.map → EngageWidget-X3NIWGMZ.js.map} +1 -1
- package/dist/{SignalCore-2UXXM4FF.js → SignalCore-K2CTZ52G.js} +61 -43
- package/dist/SignalCore-K2CTZ52G.js.map +1 -0
- package/dist/{SignalCore-2AUGAOQT.mjs → SignalCore-XABEH2GA.mjs} +55 -37
- package/dist/SignalCore-XABEH2GA.mjs.map +1 -0
- package/dist/SitemapSync-4BALOI4B.js +16 -0
- package/dist/{SitemapSync-WQJMHJNY.js.map → SitemapSync-4BALOI4B.js.map} +1 -1
- package/dist/SitemapSync-OQPRGBG3.mjs +7 -0
- package/dist/{SitemapSync-RBHXPQAQ.mjs.map → SitemapSync-OQPRGBG3.mjs.map} +1 -1
- package/dist/analytics/index.js +13 -12
- package/dist/analytics/index.mjs +6 -5
- package/dist/{chunk-2RLG4KBJ.mjs → chunk-3SIIYHVE.mjs} +5 -5
- package/dist/{chunk-2RLG4KBJ.mjs.map → chunk-3SIIYHVE.mjs.map} +1 -1
- package/dist/{chunk-PLQ4CUNI.mjs → chunk-472FTEPP.mjs} +4 -4
- package/dist/{chunk-PLQ4CUNI.mjs.map → chunk-472FTEPP.mjs.map} +1 -1
- package/dist/{chunk-BOTWTNHW.js → chunk-56X3IUOL.js} +16 -16
- package/dist/{chunk-BOTWTNHW.js.map → chunk-56X3IUOL.js.map} +1 -1
- package/dist/{chunk-QOFC3BYG.js → chunk-5SJKYZID.js} +9 -28
- package/dist/chunk-5SJKYZID.js.map +1 -0
- package/dist/{chunk-SLOZWRWU.mjs → chunk-5WEADCND.mjs} +4 -4
- package/dist/{chunk-SLOZWRWU.mjs.map → chunk-5WEADCND.mjs.map} +1 -1
- package/dist/chunk-6EDVCUAG.mjs +29 -0
- package/dist/chunk-6EDVCUAG.mjs.map +1 -0
- package/dist/{chunk-EL6ZMYD2.js → chunk-6Q72JAL2.js} +8 -8
- package/dist/{chunk-EL6ZMYD2.js.map → chunk-6Q72JAL2.js.map} +1 -1
- package/dist/{chunk-BPKDRTEA.js → chunk-7523LJ52.js} +9 -9
- package/dist/{chunk-BPKDRTEA.js.map → chunk-7523LJ52.js.map} +1 -1
- package/dist/{chunk-EM4DFSC5.js → chunk-7UYUVHCR.js} +4 -4
- package/dist/{chunk-EM4DFSC5.js.map → chunk-7UYUVHCR.js.map} +1 -1
- package/dist/{chunk-EE6SFAZP.mjs → chunk-AX4AROLH.mjs} +5 -14
- package/dist/chunk-AX4AROLH.mjs.map +1 -0
- package/dist/{chunk-G4FJJK6J.js → chunk-C37DFD5F.js} +14 -14
- package/dist/{chunk-G4FJJK6J.js.map → chunk-C37DFD5F.js.map} +1 -1
- package/dist/chunk-CDETOO5W.js +8 -0
- package/dist/{chunk-EOENN46G.js.map → chunk-CDETOO5W.js.map} +1 -1
- package/dist/{chunk-I2M533CW.js → chunk-E4VA4FYW.js} +8 -8
- package/dist/{chunk-I2M533CW.js.map → chunk-E4VA4FYW.js.map} +1 -1
- package/dist/chunk-EU2UGO25.js +52 -0
- package/dist/chunk-EU2UGO25.js.map +1 -0
- package/dist/{chunk-NNPF4KRZ.mjs → chunk-G3G4HQT3.mjs} +49 -5
- package/dist/chunk-G3G4HQT3.mjs.map +1 -0
- package/dist/{chunk-XOCQANYF.js → chunk-HV7IYMXK.js} +61 -17
- package/dist/chunk-HV7IYMXK.js.map +1 -0
- package/dist/{chunk-K2P7KPEQ.js → chunk-IB34IYMN.js} +14 -31
- package/dist/chunk-IB34IYMN.js.map +1 -0
- package/dist/chunk-IBMOX3YE.mjs +48 -0
- package/dist/chunk-IBMOX3YE.mjs.map +1 -0
- package/dist/{chunk-6BDXN4X3.mjs → chunk-IURQA3H5.mjs} +3 -3
- package/dist/{chunk-6BDXN4X3.mjs.map → chunk-IURQA3H5.mjs.map} +1 -1
- package/dist/chunk-JEOKDUPQ.js +45 -0
- package/dist/chunk-JEOKDUPQ.js.map +1 -0
- package/dist/{chunk-5KVT66R6.mjs → chunk-JRUNONG5.mjs} +3 -3
- package/dist/{chunk-5KVT66R6.mjs.map → chunk-JRUNONG5.mjs.map} +1 -1
- package/dist/chunk-LOS5D6B3.js +72 -0
- package/dist/chunk-LOS5D6B3.js.map +1 -0
- package/dist/{chunk-WEHJRE3H.mjs → chunk-LQHZFUZJ.mjs} +8 -25
- package/dist/chunk-LQHZFUZJ.mjs.map +1 -0
- package/dist/{chunk-P3QZRG2Z.js → chunk-MCDQM75H.js} +15 -15
- package/dist/{chunk-P3QZRG2Z.js.map → chunk-MCDQM75H.js.map} +1 -1
- package/dist/{chunk-6UK5JIKE.js → chunk-MJV6RC7I.js} +3 -3
- package/dist/{chunk-6UK5JIKE.js.map → chunk-MJV6RC7I.js.map} +1 -1
- package/dist/{chunk-PUGRSMAV.mjs → chunk-O4RVFKQW.mjs} +3 -3
- package/dist/{chunk-PUGRSMAV.mjs.map → chunk-O4RVFKQW.mjs.map} +1 -1
- package/dist/{chunk-WAYG3LS2.js → chunk-O7B7IEXY.js} +14 -14
- package/dist/{chunk-WAYG3LS2.js.map → chunk-O7B7IEXY.js.map} +1 -1
- package/dist/{chunk-M4HLDK62.mjs → chunk-PYA4BR2N.mjs} +6 -25
- package/dist/chunk-PYA4BR2N.mjs.map +1 -0
- package/dist/{chunk-X72PFC4X.mjs → chunk-QE53WRXW.mjs} +4 -4
- package/dist/{chunk-X72PFC4X.mjs.map → chunk-QE53WRXW.mjs.map} +1 -1
- package/dist/{chunk-DLJZYWP3.mjs → chunk-QGV7FEUI.mjs} +7 -43
- package/dist/chunk-QGV7FEUI.mjs.map +1 -0
- package/dist/chunk-QJU6HBSK.mjs +39 -0
- package/dist/chunk-QJU6HBSK.mjs.map +1 -0
- package/dist/{chunk-FX3VR4BF.js → chunk-RMUOY6XB.js} +7 -7
- package/dist/{chunk-FX3VR4BF.js.map → chunk-RMUOY6XB.js.map} +1 -1
- package/dist/{chunk-ILLOVVVP.mjs → chunk-VE5XLADP.mjs} +3 -3
- package/dist/{chunk-ILLOVVVP.mjs.map → chunk-VE5XLADP.mjs.map} +1 -1
- package/dist/chunk-VJMPIRSD.js +31 -0
- package/dist/chunk-VJMPIRSD.js.map +1 -0
- package/dist/{chunk-Z26CMPBX.js → chunk-VMVEL4WU.js} +7 -43
- package/dist/chunk-VMVEL4WU.js.map +1 -0
- package/dist/{chunk-ZCZEKWFW.mjs → chunk-VNQFKBKF.mjs} +3 -3
- package/dist/{chunk-ZCZEKWFW.mjs.map → chunk-VNQFKBKF.mjs.map} +1 -1
- package/dist/{chunk-FO6Y27AH.mjs → chunk-WS4GNH3F.mjs} +4 -4
- package/dist/{chunk-FO6Y27AH.mjs.map → chunk-WS4GNH3F.mjs.map} +1 -1
- package/dist/{chunk-AXFM4MFK.mjs → chunk-WVAY4ZWC.mjs} +9 -9
- package/dist/{chunk-AXFM4MFK.mjs.map → chunk-WVAY4ZWC.mjs.map} +1 -1
- package/dist/{chunk-3BNFUMLL.js → chunk-XB57AM6T.js} +5 -14
- package/dist/chunk-XB57AM6T.js.map +1 -0
- package/dist/chunk-XIJ4F6IE.mjs +63 -0
- package/dist/chunk-XIJ4F6IE.mjs.map +1 -0
- package/dist/{chunk-M2EGCGIT.js → chunk-XKV73RDH.js} +9 -7
- package/dist/{chunk-M2EGCGIT.js.map → chunk-XKV73RDH.js.map} +1 -1
- package/dist/chunk-YCQHE2XT.mjs +6 -0
- package/dist/{chunk-S4FJIG6Y.mjs.map → chunk-YCQHE2XT.mjs.map} +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +1 -1
- package/dist/client/index.js +6 -5
- package/dist/client/index.mjs +2 -1
- package/dist/commerce/index.js +52 -51
- package/dist/commerce/index.mjs +7 -6
- package/dist/discover-Q5RWC27Y.js +21 -0
- package/dist/discover-Q5RWC27Y.js.map +1 -0
- package/dist/discover-ROPVYJJP.mjs +4 -0
- package/dist/discover-ROPVYJJP.mjs.map +1 -0
- package/dist/engage/index.js +10 -9
- package/dist/engage/index.js.map +1 -1
- package/dist/engage/index.mjs +7 -6
- package/dist/engage/index.mjs.map +1 -1
- package/dist/fleet/index.js +7 -7
- package/dist/fleet/index.mjs +3 -3
- package/dist/forms/index.js +13 -12
- package/dist/forms/index.js.map +1 -1
- package/dist/forms/index.mjs +5 -4
- package/dist/forms/index.mjs.map +1 -1
- package/dist/images/index.js +10 -10
- package/dist/images/index.mjs +3 -3
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +74 -64
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +12 -10
- package/dist/index.mjs.map +1 -1
- package/dist/landing/index.d.mts +21 -0
- package/dist/landing/index.d.ts +21 -0
- package/dist/landing/index.js +21 -0
- package/dist/landing/index.js.map +1 -0
- package/dist/landing/index.mjs +4 -0
- package/dist/landing/index.mjs.map +1 -0
- package/dist/layout/client.js +8 -7
- package/dist/layout/client.mjs +7 -6
- package/dist/layout/index.js +9 -8
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/index.mjs +7 -6
- package/dist/layout/index.mjs.map +1 -1
- package/dist/maps/index.js +4 -4
- package/dist/maps/index.mjs +2 -2
- package/dist/metadata-D_GiY8LJ.d.mts +36 -0
- package/dist/metadata-D_GiY8LJ.d.ts +36 -0
- package/dist/middleware/index.js +2 -8
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +2 -8
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/seo/client.js +5 -4
- package/dist/seo/client.mjs +4 -3
- package/dist/seo/index.js +14 -13
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/index.mjs +6 -5
- package/dist/seo/index.mjs.map +1 -1
- package/dist/seo/register-sitemap-cli.js +64 -96
- package/dist/seo/register-sitemap-cli.js.map +1 -1
- package/dist/seo/register-sitemap-cli.mjs +62 -94
- package/dist/seo/register-sitemap-cli.mjs.map +1 -1
- package/dist/seo/server.js +5 -5
- package/dist/seo/server.mjs +1 -1
- package/dist/signal/index.js +11 -11
- package/dist/signal/index.mjs +2 -2
- package/dist/sitemap/index.d.mts +15 -17
- package/dist/sitemap/index.d.ts +15 -17
- package/dist/sitemap/index.js +14 -37
- package/dist/sitemap/index.js.map +1 -1
- package/dist/sitemap/index.mjs +11 -34
- package/dist/sitemap/index.mjs.map +1 -1
- package/dist/sync/index.js +15 -15
- package/dist/sync/index.mjs +3 -3
- package/package.json +11 -1
- package/dist/AnalyticsProvider-26VS3CQR.js +0 -29
- package/dist/AnalyticsProvider-AMHG4DIE.mjs +0 -8
- package/dist/ChatWidget-NIYNSMLU.js +0 -17
- package/dist/ChatWidget-UKPEQEHB.mjs +0 -8
- package/dist/EngageWidget-33QCFWQW.js +0 -16
- package/dist/EngageWidget-S76ZMDEF.mjs +0 -7
- package/dist/SignalCore-2AUGAOQT.mjs.map +0 -1
- package/dist/SignalCore-2UXXM4FF.js.map +0 -1
- package/dist/SitemapSync-RBHXPQAQ.mjs +0 -6
- package/dist/SitemapSync-WQJMHJNY.js +0 -15
- package/dist/chunk-3BNFUMLL.js.map +0 -1
- package/dist/chunk-DLJZYWP3.mjs.map +0 -1
- package/dist/chunk-EE6SFAZP.mjs.map +0 -1
- package/dist/chunk-EOENN46G.js +0 -8
- package/dist/chunk-K2P7KPEQ.js.map +0 -1
- package/dist/chunk-M4HLDK62.mjs.map +0 -1
- package/dist/chunk-NNPF4KRZ.mjs.map +0 -1
- package/dist/chunk-QOFC3BYG.js.map +0 -1
- package/dist/chunk-S4FJIG6Y.mjs +0 -6
- package/dist/chunk-WEHJRE3H.mjs.map +0 -1
- package/dist/chunk-XOCQANYF.js.map +0 -1
- package/dist/chunk-Z26CMPBX.js.map +0 -1
package/dist/layout/client.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
5
|
-
require('../chunk-
|
|
4
|
+
var chunk56X3IUOL_js = require('../chunk-56X3IUOL.js');
|
|
5
|
+
require('../chunk-RMUOY6XB.js');
|
|
6
6
|
require('../chunk-HGNSJXWT.js');
|
|
7
7
|
require('../chunk-Q2NBD54V.js');
|
|
8
8
|
require('../chunk-WZHWWOUC.js');
|
|
9
|
-
require('../chunk-
|
|
10
|
-
require('../chunk-
|
|
11
|
-
require('../chunk-
|
|
12
|
-
require('../chunk-
|
|
9
|
+
require('../chunk-MJV6RC7I.js');
|
|
10
|
+
require('../chunk-XB57AM6T.js');
|
|
11
|
+
require('../chunk-VJMPIRSD.js');
|
|
12
|
+
require('../chunk-XKV73RDH.js');
|
|
13
|
+
require('../chunk-CDETOO5W.js');
|
|
13
14
|
require('../chunk-ZSMWDLMK.js');
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
Object.defineProperty(exports, "SiteKitClientProviders", {
|
|
18
19
|
enumerable: true,
|
|
19
|
-
get: function () { return
|
|
20
|
+
get: function () { return chunk56X3IUOL_js.SiteKitClientProviders; }
|
|
20
21
|
});
|
|
21
22
|
//# sourceMappingURL=client.js.map
|
|
22
23
|
//# sourceMappingURL=client.js.map
|
package/dist/layout/client.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
export { SiteKitClientProviders } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
export { SiteKitClientProviders } from '../chunk-WVAY4ZWC.mjs';
|
|
3
|
+
import '../chunk-QE53WRXW.mjs';
|
|
4
4
|
import '../chunk-ZVBLXXNJ.mjs';
|
|
5
5
|
import '../chunk-ZPLN537Q.mjs';
|
|
6
6
|
import '../chunk-X4OPM3SK.mjs';
|
|
7
|
-
import '../chunk-
|
|
8
|
-
import '../chunk-
|
|
9
|
-
import '../chunk-
|
|
10
|
-
import '../chunk-
|
|
7
|
+
import '../chunk-VNQFKBKF.mjs';
|
|
8
|
+
import '../chunk-AX4AROLH.mjs';
|
|
9
|
+
import '../chunk-6EDVCUAG.mjs';
|
|
10
|
+
import '../chunk-WS4GNH3F.mjs';
|
|
11
|
+
import '../chunk-YCQHE2XT.mjs';
|
|
11
12
|
import '../chunk-4XPGGLVP.mjs';
|
|
12
13
|
//# sourceMappingURL=client.mjs.map
|
|
13
14
|
//# sourceMappingURL=client.mjs.map
|
package/dist/layout/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
require('../chunk-
|
|
3
|
+
var chunk56X3IUOL_js = require('../chunk-56X3IUOL.js');
|
|
4
|
+
require('../chunk-RMUOY6XB.js');
|
|
5
5
|
require('../chunk-HGNSJXWT.js');
|
|
6
6
|
require('../chunk-Q2NBD54V.js');
|
|
7
7
|
require('../chunk-WZHWWOUC.js');
|
|
8
8
|
var chunkGCPPXUAI_js = require('../chunk-GCPPXUAI.js');
|
|
9
|
-
require('../chunk-
|
|
9
|
+
require('../chunk-MJV6RC7I.js');
|
|
10
10
|
var chunkDV2LJYFD_js = require('../chunk-DV2LJYFD.js');
|
|
11
11
|
var chunkQPIKPWAZ_js = require('../chunk-QPIKPWAZ.js');
|
|
12
|
-
require('../chunk-
|
|
13
|
-
require('../chunk-
|
|
14
|
-
|
|
12
|
+
require('../chunk-XB57AM6T.js');
|
|
13
|
+
require('../chunk-VJMPIRSD.js');
|
|
14
|
+
require('../chunk-XKV73RDH.js');
|
|
15
|
+
var chunkCDETOO5W_js = require('../chunk-CDETOO5W.js');
|
|
15
16
|
require('../chunk-ZSMWDLMK.js');
|
|
16
17
|
require('server-only');
|
|
17
18
|
var jsxRuntime = require('react/jsx-runtime');
|
|
@@ -33,7 +34,7 @@ async function resolveClientCredential(rawKey, apiUrl) {
|
|
|
33
34
|
headers: {
|
|
34
35
|
"content-type": "application/json",
|
|
35
36
|
"x-api-key": rawKey,
|
|
36
|
-
"x-sitekit-version":
|
|
37
|
+
"x-sitekit-version": chunkCDETOO5W_js.SITE_KIT_VERSION
|
|
37
38
|
},
|
|
38
39
|
cache: "no-store",
|
|
39
40
|
signal: typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(MINT_TIMEOUT_MS) : void 0
|
|
@@ -91,7 +92,7 @@ async function SiteKitLayout({
|
|
|
91
92
|
favicon && /* @__PURE__ */ jsxRuntime.jsx(chunkGCPPXUAI_js.ManagedFavicon, { apiKey: resolvedApiKey, apiUrl: resolvedApiUrl }),
|
|
92
93
|
managedScripts && /* @__PURE__ */ jsxRuntime.jsx(chunkDV2LJYFD_js.ManagedScripts, { position: "head" }),
|
|
93
94
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
94
|
-
|
|
95
|
+
chunk56X3IUOL_js.SiteKitClientProviders,
|
|
95
96
|
{
|
|
96
97
|
apiKey: clientCredential,
|
|
97
98
|
apiUrl: resolvedApiUrl,
|
package/dist/layout/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/mint-site-token.ts","../../src/layout/SiteKitLayout.tsx"],"names":["SITE_KIT_VERSION","resolveApiOrigin","jsxs","Fragment","jsx","ManagedFavicon","ManagedScripts","SiteKitClientProviders"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @sonordev/site-kit/server — Client-token minting (server-only)\n *\n * SiteKitLayout holds the raw `SONOR_API_KEY` server-side but must NOT ship it to\n * the browser. This mints a short-lived, project-scoped token (server-to-server,\n * authenticated with the raw key, no browser Origin so it passes domain-binding)\n * and returns THAT for the layout to inject instead.\n *\n * `import 'server-only'` makes importing this into a client bundle a hard build\n * error — the raw key stays on the server.\n *\n * Resilience: if minting is unavailable (older API without the endpoint, token\n * auth not configured, transient outage) it returns the RAW KEY, so a build never\n * breaks. Domain-binding is log-only by default, so the raw key still works —\n * worst case is exactly pre-3.2 behavior. Memoized per raw key for the process,\n * so an entire `next build` mints ~once and every static page reuses one seed\n * (the client refreshes on the far side as needed).\n */\nimport 'server-only'\nimport { SITE_KIT_VERSION } from '../shared/version'\n\nconst MINT_PATH = '/api/public/site-token'\nconst MINT_TIMEOUT_MS = 4_000\nconst SITE_TOKEN_PREFIX = 'st1.'\n\ninterface CacheEntry {\n credential: string\n expiresAt: number\n}\nconst cache = new Map<string, CacheEntry>()\n\nexport async function resolveClientCredential(\n rawKey: string,\n apiUrl: string,\n): Promise<string> {\n if (!rawKey) return ''\n // Env should hold a raw key; if it already looks like a token, pass it through.\n if (rawKey.startsWith(SITE_TOKEN_PREFIX)) return rawKey\n\n const now = Date.now()\n const cached = cache.get(rawKey)\n if (cached && cached.expiresAt > now) return cached.credential\n\n const base = (apiUrl || 'https://api.sonor.io').replace(/\\/+$/, '')\n try {\n const res = await fetch(`${base}${MINT_PATH}`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': rawKey,\n 'x-sitekit-version': SITE_KIT_VERSION,\n },\n cache: 'no-store',\n signal:\n typeof AbortSignal !== 'undefined' && AbortSignal.timeout\n ? AbortSignal.timeout(MINT_TIMEOUT_MS)\n : undefined,\n })\n\n if (res.ok) {\n const data: any = await res.json().catch(() => null)\n const token = data?.token\n const expiresIn = typeof data?.expiresIn === 'number' ? data.expiresIn : 3600\n if (typeof token === 'string' && token.startsWith(SITE_TOKEN_PREFIX)) {\n cache.set(rawKey, {\n credential: token,\n expiresAt: now + Math.min((expiresIn * 1000) / 2, 30 * 60_000),\n })\n return token\n }\n } else if (res.status !== 404) {\n // 404 = older API without the endpoint (expected during rollout). Anything\n // else is worth a server-side note. Never logs the key value.\n console.warn(\n `[SiteKitLayout] Token mint returned ${res.status}; injecting raw key for this render.`,\n )\n }\n } catch {\n console.warn('[SiteKitLayout] Token mint unavailable; injecting raw key for this render.')\n }\n\n // Fallback: the raw key (pre-3.2 behavior). Short cache so a transient outage\n // retries soon rather than pinning the raw key for the whole build.\n cache.set(rawKey, { credential: rawKey, expiresAt: now + 60_000 })\n return rawKey\n}\n","/**\n * @sonordev/site-kit/layout — SiteKitLayout\n *\n * The master layout component. A single RSC that auto-composes every\n * site-kit feature your layout needs:\n *\n * Server-side (RSC):\n * - ManagedFavicon (link tags from Portal logo)\n * - ManagedScripts position=\"head\" (tracking pixels, analytics tags)\n * - ManagedScripts position=\"body-end\"\n *\n * Client-side (island):\n * - AnalyticsProvider (page views, scroll depth, heatmap, web vitals)\n * - EngageWidget (popups, nudges, chat)\n * - SignalBridge (A/B experiments, behavior tracking)\n * - SitemapSync (keeps Portal seo_pages in sync)\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { SiteKitLayout } from '@sonordev/site-kit/layout'\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html lang=\"en\">\n * <body>\n * <SiteKitLayout>\n * <Header />\n * <main>{children}</main>\n * <Footer />\n * </SiteKitLayout>\n * </body>\n * </html>\n * )\n * }\n * ```\n *\n * @example Opt out of specific features:\n * ```tsx\n * <SiteKitLayout engage={false} signal={false}>\n * {children}\n * </SiteKitLayout>\n * ```\n */\n\nimport * as React from 'react'\nimport { ManagedFavicon } from '../images/ManagedFavicon'\nimport { ManagedScripts } from '../seo/ManagedScripts'\nimport { resolveApiOrigin } from '../server/server-fetch'\nimport { resolveClientCredential } from '../server/mint-site-token'\nimport { SiteKitClientProviders } from './SiteKitClientProviders'\nimport type { AnalyticsConfig, EngageConfig, SignalConfig } from './types'\n\nexport interface SiteKitLayoutProps {\n children: React.ReactNode\n\n /** Portal API key. Defaults to SONOR_API_KEY env var. */\n apiKey?: string\n /** Portal API URL. Defaults to env or https://api.sonor.io */\n apiUrl?: string\n /** Project ID (for Engage chat room routing). Auto-resolved if not provided. */\n projectId?: string\n\n /** Analytics tracking. true (default) | false | config object */\n analytics?: boolean | AnalyticsConfig\n /** Engage widgets (popups, chat). true (default) | false | config object */\n engage?: boolean | EngageConfig\n /** Signal AI (experiments, behavior). false (default) | true | config object */\n signal?: boolean | SignalConfig\n /** SitemapSync client component. Default: true */\n sitemapSync?: boolean\n /**\n * Fleet heartbeat — reports this site's kit version + active modules to Sonor\n * once per session so the platform can track version drift. Default: true.\n * Deferred like Analytics; never in the hydration path.\n */\n fleet?: boolean\n /**\n * Defer client modules (Analytics, Engage, SitemapSync) until window load +\n * idle or the first user interaction, keeping their JS and mount work out of\n * the critical hydration path. Default: true. Pass false to restore legacy\n * eager mounting. SignalBridge is never deferred.\n */\n defer?: boolean\n /** ManagedFavicon in <head>. Default: true */\n favicon?: boolean\n /** ManagedScripts (head + body-end). Default: true */\n managedScripts?: boolean\n\n /** Debug mode — logs to console. Default: false */\n debug?: boolean\n /**\n * When true, append a screen-reader-only link to `/llms.txt` after main content.\n * Default false — prefer `buildAiDiscoveryHeaders()` for discovery.\n */\n showLlmsTxtFooterLink?: boolean\n}\n\nexport async function SiteKitLayout({\n children,\n apiKey,\n apiUrl,\n projectId,\n analytics = true,\n engage = true,\n signal = false,\n sitemapSync = true,\n fleet = true,\n defer = true,\n favicon = true,\n managedScripts = true,\n debug = false,\n showLlmsTxtFooterLink = false,\n}: SiteKitLayoutProps) {\n // Resolve API config from env (server-side, safe)\n const resolvedApiKey =\n apiKey ??\n process.env.SONOR_API_KEY ??\n ''\n const resolvedApiUrl =\n apiUrl ??\n process.env.SONOR_API_URL ??\n 'https://api.sonor.io'\n\n if (!resolvedApiKey && debug) {\n console.warn(\n '[SiteKitLayout] No API key found. Set SONOR_API_KEY.',\n )\n }\n\n const apiOrigin = resolveApiOrigin(resolvedApiUrl)\n\n // Mint a short-lived client token so the raw key never reaches the browser.\n // Falls back to the raw key if minting is unavailable (older API / transient\n // outage) — the build never breaks. The client refreshes the token as needed.\n const clientCredential = await resolveClientCredential(resolvedApiKey, resolvedApiUrl)\n\n return (\n <>\n <link rel=\"preconnect\" href={apiOrigin} crossOrigin=\"anonymous\" />\n <link rel=\"dns-prefetch\" href={apiOrigin} />\n {/* Server-rendered head components */}\n {favicon && <ManagedFavicon apiKey={resolvedApiKey} apiUrl={resolvedApiUrl} />}\n {managedScripts && <ManagedScripts position=\"head\" />}\n\n {/* Client-side providers island — receives the minted token (or raw-key\n fallback), never the raw key when minting succeeds. */}\n <SiteKitClientProviders\n apiKey={clientCredential}\n apiUrl={resolvedApiUrl}\n projectId={projectId}\n analytics={analytics}\n engage={engage}\n signal={signal}\n sitemapSync={sitemapSync}\n fleet={fleet}\n defer={defer}\n debug={debug}\n >\n {children}\n {showLlmsTxtFooterLink ? (\n <footer className=\"sr-only\" aria-hidden=\"true\">\n <a href=\"/llms.txt\">llms.txt</a>\n </footer>\n ) : null}\n </SiteKitClientProviders>\n\n {/* Server-rendered body-end scripts */}\n {managedScripts && <ManagedScripts position=\"body-end\" />}\n </>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/mint-site-token.ts","../../src/layout/SiteKitLayout.tsx"],"names":["SITE_KIT_VERSION","resolveApiOrigin","jsxs","Fragment","jsx","ManagedFavicon","ManagedScripts","SiteKitClientProviders"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @sonordev/site-kit/server — Client-token minting (server-only)\n *\n * SiteKitLayout holds the raw `SONOR_API_KEY` server-side but must NOT ship it to\n * the browser. This mints a short-lived, project-scoped token (server-to-server,\n * authenticated with the raw key, no browser Origin so it passes domain-binding)\n * and returns THAT for the layout to inject instead.\n *\n * `import 'server-only'` makes importing this into a client bundle a hard build\n * error — the raw key stays on the server.\n *\n * Resilience: if minting is unavailable (older API without the endpoint, token\n * auth not configured, transient outage) it returns the RAW KEY, so a build never\n * breaks. Domain-binding is log-only by default, so the raw key still works —\n * worst case is exactly pre-3.2 behavior. Memoized per raw key for the process,\n * so an entire `next build` mints ~once and every static page reuses one seed\n * (the client refreshes on the far side as needed).\n */\nimport 'server-only'\nimport { SITE_KIT_VERSION } from '../shared/version'\n\nconst MINT_PATH = '/api/public/site-token'\nconst MINT_TIMEOUT_MS = 4_000\nconst SITE_TOKEN_PREFIX = 'st1.'\n\ninterface CacheEntry {\n credential: string\n expiresAt: number\n}\nconst cache = new Map<string, CacheEntry>()\n\nexport async function resolveClientCredential(\n rawKey: string,\n apiUrl: string,\n): Promise<string> {\n if (!rawKey) return ''\n // Env should hold a raw key; if it already looks like a token, pass it through.\n if (rawKey.startsWith(SITE_TOKEN_PREFIX)) return rawKey\n\n const now = Date.now()\n const cached = cache.get(rawKey)\n if (cached && cached.expiresAt > now) return cached.credential\n\n const base = (apiUrl || 'https://api.sonor.io').replace(/\\/+$/, '')\n try {\n const res = await fetch(`${base}${MINT_PATH}`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': rawKey,\n 'x-sitekit-version': SITE_KIT_VERSION,\n },\n cache: 'no-store',\n signal:\n typeof AbortSignal !== 'undefined' && AbortSignal.timeout\n ? AbortSignal.timeout(MINT_TIMEOUT_MS)\n : undefined,\n })\n\n if (res.ok) {\n const data: any = await res.json().catch(() => null)\n const token = data?.token\n const expiresIn = typeof data?.expiresIn === 'number' ? data.expiresIn : 3600\n if (typeof token === 'string' && token.startsWith(SITE_TOKEN_PREFIX)) {\n cache.set(rawKey, {\n credential: token,\n expiresAt: now + Math.min((expiresIn * 1000) / 2, 30 * 60_000),\n })\n return token\n }\n } else if (res.status !== 404) {\n // 404 = older API without the endpoint (expected during rollout). Anything\n // else is worth a server-side note. Never logs the key value.\n console.warn(\n `[SiteKitLayout] Token mint returned ${res.status}; injecting raw key for this render.`,\n )\n }\n } catch {\n console.warn('[SiteKitLayout] Token mint unavailable; injecting raw key for this render.')\n }\n\n // Fallback: the raw key (pre-3.2 behavior). Short cache so a transient outage\n // retries soon rather than pinning the raw key for the whole build.\n cache.set(rawKey, { credential: rawKey, expiresAt: now + 60_000 })\n return rawKey\n}\n","/**\n * @sonordev/site-kit/layout — SiteKitLayout\n *\n * The master layout component. A single RSC that auto-composes every\n * site-kit feature your layout needs:\n *\n * Server-side (RSC):\n * - ManagedFavicon (link tags from Portal logo)\n * - ManagedScripts position=\"head\" (tracking pixels, analytics tags)\n * - ManagedScripts position=\"body-end\"\n *\n * Client-side (island):\n * - AnalyticsProvider (page views, scroll depth, heatmap, web vitals)\n * - EngageWidget (popups, nudges, chat)\n * - SignalBridge (A/B experiments, behavior tracking)\n * - SitemapSync (keeps Portal seo_pages in sync)\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { SiteKitLayout } from '@sonordev/site-kit/layout'\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html lang=\"en\">\n * <body>\n * <SiteKitLayout>\n * <Header />\n * <main>{children}</main>\n * <Footer />\n * </SiteKitLayout>\n * </body>\n * </html>\n * )\n * }\n * ```\n *\n * @example Opt out of specific features:\n * ```tsx\n * <SiteKitLayout engage={false} signal={false}>\n * {children}\n * </SiteKitLayout>\n * ```\n */\n\nimport * as React from 'react'\nimport { ManagedFavicon } from '../images/ManagedFavicon'\nimport { ManagedScripts } from '../seo/ManagedScripts'\nimport { resolveApiOrigin } from '../server/server-fetch'\nimport { resolveClientCredential } from '../server/mint-site-token'\nimport { SiteKitClientProviders } from './SiteKitClientProviders'\nimport type { AnalyticsConfig, EngageConfig, SignalConfig } from './types'\n\nexport interface SiteKitLayoutProps {\n children: React.ReactNode\n\n /** Portal API key. Defaults to SONOR_API_KEY env var. */\n apiKey?: string\n /** Portal API URL. Defaults to env or https://api.sonor.io */\n apiUrl?: string\n /** Project ID (for Engage chat room routing). Auto-resolved if not provided. */\n projectId?: string\n\n /** Analytics tracking. true (default) | false | config object */\n analytics?: boolean | AnalyticsConfig\n /** Engage widgets (popups, chat). true (default) | false | config object */\n engage?: boolean | EngageConfig\n /** Signal AI (experiments, behavior). false (default) | true | config object */\n signal?: boolean | SignalConfig\n /** SitemapSync client component. Default: true */\n sitemapSync?: boolean\n /**\n * Fleet heartbeat — reports this site's kit version + active modules to Sonor\n * once per session so the platform can track version drift. Default: true.\n * Deferred like Analytics; never in the hydration path.\n */\n fleet?: boolean\n /**\n * Defer client modules (Analytics, Engage, SitemapSync) until window load +\n * idle or the first user interaction, keeping their JS and mount work out of\n * the critical hydration path. Default: true. Pass false to restore legacy\n * eager mounting. SignalBridge is never deferred.\n */\n defer?: boolean\n /** ManagedFavicon in <head>. Default: true */\n favicon?: boolean\n /** ManagedScripts (head + body-end). Default: true */\n managedScripts?: boolean\n\n /** Debug mode — logs to console. Default: false */\n debug?: boolean\n /**\n * When true, append a screen-reader-only link to `/llms.txt` after main content.\n * Default false — prefer `buildAiDiscoveryHeaders()` for discovery.\n */\n showLlmsTxtFooterLink?: boolean\n}\n\nexport async function SiteKitLayout({\n children,\n apiKey,\n apiUrl,\n projectId,\n analytics = true,\n engage = true,\n signal = false,\n sitemapSync = true,\n fleet = true,\n defer = true,\n favicon = true,\n managedScripts = true,\n debug = false,\n showLlmsTxtFooterLink = false,\n}: SiteKitLayoutProps) {\n // Resolve API config from env (server-side, safe)\n const resolvedApiKey =\n apiKey ??\n process.env.SONOR_API_KEY ??\n ''\n const resolvedApiUrl =\n apiUrl ??\n process.env.SONOR_API_URL ??\n 'https://api.sonor.io'\n\n if (!resolvedApiKey && debug) {\n console.warn(\n '[SiteKitLayout] No API key found. Set SONOR_API_KEY.',\n )\n }\n\n const apiOrigin = resolveApiOrigin(resolvedApiUrl)\n\n // Mint a short-lived client token so the raw key never reaches the browser.\n // Falls back to the raw key if minting is unavailable (older API / transient\n // outage) — the build never breaks. The client refreshes the token as needed.\n const clientCredential = await resolveClientCredential(resolvedApiKey, resolvedApiUrl)\n\n return (\n <>\n <link rel=\"preconnect\" href={apiOrigin} crossOrigin=\"anonymous\" />\n <link rel=\"dns-prefetch\" href={apiOrigin} />\n {/* Server-rendered head components */}\n {favicon && <ManagedFavicon apiKey={resolvedApiKey} apiUrl={resolvedApiUrl} />}\n {managedScripts && <ManagedScripts position=\"head\" />}\n\n {/* Client-side providers island — receives the minted token (or raw-key\n fallback), never the raw key when minting succeeds. */}\n <SiteKitClientProviders\n apiKey={clientCredential}\n apiUrl={resolvedApiUrl}\n projectId={projectId}\n analytics={analytics}\n engage={engage}\n signal={signal}\n sitemapSync={sitemapSync}\n fleet={fleet}\n defer={defer}\n debug={debug}\n >\n {children}\n {showLlmsTxtFooterLink ? (\n <footer className=\"sr-only\" aria-hidden=\"true\">\n <a href=\"/llms.txt\">llms.txt</a>\n </footer>\n ) : null}\n </SiteKitClientProviders>\n\n {/* Server-rendered body-end scripts */}\n {managedScripts && <ManagedScripts position=\"body-end\" />}\n </>\n )\n}\n"]}
|
package/dist/layout/index.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { SiteKitClientProviders } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
1
|
+
import { SiteKitClientProviders } from '../chunk-WVAY4ZWC.mjs';
|
|
2
|
+
import '../chunk-QE53WRXW.mjs';
|
|
3
3
|
import '../chunk-ZVBLXXNJ.mjs';
|
|
4
4
|
import '../chunk-ZPLN537Q.mjs';
|
|
5
5
|
import '../chunk-X4OPM3SK.mjs';
|
|
6
6
|
import { ManagedFavicon } from '../chunk-CU6OGBKK.mjs';
|
|
7
|
-
import '../chunk-
|
|
7
|
+
import '../chunk-VNQFKBKF.mjs';
|
|
8
8
|
import { ManagedScripts } from '../chunk-6PJTLBRR.mjs';
|
|
9
9
|
import { resolveApiOrigin } from '../chunk-UDBUK5ZJ.mjs';
|
|
10
|
-
import '../chunk-
|
|
11
|
-
import '../chunk-
|
|
12
|
-
import
|
|
10
|
+
import '../chunk-AX4AROLH.mjs';
|
|
11
|
+
import '../chunk-6EDVCUAG.mjs';
|
|
12
|
+
import '../chunk-WS4GNH3F.mjs';
|
|
13
|
+
import { SITE_KIT_VERSION } from '../chunk-YCQHE2XT.mjs';
|
|
13
14
|
import '../chunk-4XPGGLVP.mjs';
|
|
14
15
|
import 'server-only';
|
|
15
16
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/mint-site-token.ts","../../src/layout/SiteKitLayout.tsx"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @sonordev/site-kit/server — Client-token minting (server-only)\n *\n * SiteKitLayout holds the raw `SONOR_API_KEY` server-side but must NOT ship it to\n * the browser. This mints a short-lived, project-scoped token (server-to-server,\n * authenticated with the raw key, no browser Origin so it passes domain-binding)\n * and returns THAT for the layout to inject instead.\n *\n * `import 'server-only'` makes importing this into a client bundle a hard build\n * error — the raw key stays on the server.\n *\n * Resilience: if minting is unavailable (older API without the endpoint, token\n * auth not configured, transient outage) it returns the RAW KEY, so a build never\n * breaks. Domain-binding is log-only by default, so the raw key still works —\n * worst case is exactly pre-3.2 behavior. Memoized per raw key for the process,\n * so an entire `next build` mints ~once and every static page reuses one seed\n * (the client refreshes on the far side as needed).\n */\nimport 'server-only'\nimport { SITE_KIT_VERSION } from '../shared/version'\n\nconst MINT_PATH = '/api/public/site-token'\nconst MINT_TIMEOUT_MS = 4_000\nconst SITE_TOKEN_PREFIX = 'st1.'\n\ninterface CacheEntry {\n credential: string\n expiresAt: number\n}\nconst cache = new Map<string, CacheEntry>()\n\nexport async function resolveClientCredential(\n rawKey: string,\n apiUrl: string,\n): Promise<string> {\n if (!rawKey) return ''\n // Env should hold a raw key; if it already looks like a token, pass it through.\n if (rawKey.startsWith(SITE_TOKEN_PREFIX)) return rawKey\n\n const now = Date.now()\n const cached = cache.get(rawKey)\n if (cached && cached.expiresAt > now) return cached.credential\n\n const base = (apiUrl || 'https://api.sonor.io').replace(/\\/+$/, '')\n try {\n const res = await fetch(`${base}${MINT_PATH}`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': rawKey,\n 'x-sitekit-version': SITE_KIT_VERSION,\n },\n cache: 'no-store',\n signal:\n typeof AbortSignal !== 'undefined' && AbortSignal.timeout\n ? AbortSignal.timeout(MINT_TIMEOUT_MS)\n : undefined,\n })\n\n if (res.ok) {\n const data: any = await res.json().catch(() => null)\n const token = data?.token\n const expiresIn = typeof data?.expiresIn === 'number' ? data.expiresIn : 3600\n if (typeof token === 'string' && token.startsWith(SITE_TOKEN_PREFIX)) {\n cache.set(rawKey, {\n credential: token,\n expiresAt: now + Math.min((expiresIn * 1000) / 2, 30 * 60_000),\n })\n return token\n }\n } else if (res.status !== 404) {\n // 404 = older API without the endpoint (expected during rollout). Anything\n // else is worth a server-side note. Never logs the key value.\n console.warn(\n `[SiteKitLayout] Token mint returned ${res.status}; injecting raw key for this render.`,\n )\n }\n } catch {\n console.warn('[SiteKitLayout] Token mint unavailable; injecting raw key for this render.')\n }\n\n // Fallback: the raw key (pre-3.2 behavior). Short cache so a transient outage\n // retries soon rather than pinning the raw key for the whole build.\n cache.set(rawKey, { credential: rawKey, expiresAt: now + 60_000 })\n return rawKey\n}\n","/**\n * @sonordev/site-kit/layout — SiteKitLayout\n *\n * The master layout component. A single RSC that auto-composes every\n * site-kit feature your layout needs:\n *\n * Server-side (RSC):\n * - ManagedFavicon (link tags from Portal logo)\n * - ManagedScripts position=\"head\" (tracking pixels, analytics tags)\n * - ManagedScripts position=\"body-end\"\n *\n * Client-side (island):\n * - AnalyticsProvider (page views, scroll depth, heatmap, web vitals)\n * - EngageWidget (popups, nudges, chat)\n * - SignalBridge (A/B experiments, behavior tracking)\n * - SitemapSync (keeps Portal seo_pages in sync)\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { SiteKitLayout } from '@sonordev/site-kit/layout'\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html lang=\"en\">\n * <body>\n * <SiteKitLayout>\n * <Header />\n * <main>{children}</main>\n * <Footer />\n * </SiteKitLayout>\n * </body>\n * </html>\n * )\n * }\n * ```\n *\n * @example Opt out of specific features:\n * ```tsx\n * <SiteKitLayout engage={false} signal={false}>\n * {children}\n * </SiteKitLayout>\n * ```\n */\n\nimport * as React from 'react'\nimport { ManagedFavicon } from '../images/ManagedFavicon'\nimport { ManagedScripts } from '../seo/ManagedScripts'\nimport { resolveApiOrigin } from '../server/server-fetch'\nimport { resolveClientCredential } from '../server/mint-site-token'\nimport { SiteKitClientProviders } from './SiteKitClientProviders'\nimport type { AnalyticsConfig, EngageConfig, SignalConfig } from './types'\n\nexport interface SiteKitLayoutProps {\n children: React.ReactNode\n\n /** Portal API key. Defaults to SONOR_API_KEY env var. */\n apiKey?: string\n /** Portal API URL. Defaults to env or https://api.sonor.io */\n apiUrl?: string\n /** Project ID (for Engage chat room routing). Auto-resolved if not provided. */\n projectId?: string\n\n /** Analytics tracking. true (default) | false | config object */\n analytics?: boolean | AnalyticsConfig\n /** Engage widgets (popups, chat). true (default) | false | config object */\n engage?: boolean | EngageConfig\n /** Signal AI (experiments, behavior). false (default) | true | config object */\n signal?: boolean | SignalConfig\n /** SitemapSync client component. Default: true */\n sitemapSync?: boolean\n /**\n * Fleet heartbeat — reports this site's kit version + active modules to Sonor\n * once per session so the platform can track version drift. Default: true.\n * Deferred like Analytics; never in the hydration path.\n */\n fleet?: boolean\n /**\n * Defer client modules (Analytics, Engage, SitemapSync) until window load +\n * idle or the first user interaction, keeping their JS and mount work out of\n * the critical hydration path. Default: true. Pass false to restore legacy\n * eager mounting. SignalBridge is never deferred.\n */\n defer?: boolean\n /** ManagedFavicon in <head>. Default: true */\n favicon?: boolean\n /** ManagedScripts (head + body-end). Default: true */\n managedScripts?: boolean\n\n /** Debug mode — logs to console. Default: false */\n debug?: boolean\n /**\n * When true, append a screen-reader-only link to `/llms.txt` after main content.\n * Default false — prefer `buildAiDiscoveryHeaders()` for discovery.\n */\n showLlmsTxtFooterLink?: boolean\n}\n\nexport async function SiteKitLayout({\n children,\n apiKey,\n apiUrl,\n projectId,\n analytics = true,\n engage = true,\n signal = false,\n sitemapSync = true,\n fleet = true,\n defer = true,\n favicon = true,\n managedScripts = true,\n debug = false,\n showLlmsTxtFooterLink = false,\n}: SiteKitLayoutProps) {\n // Resolve API config from env (server-side, safe)\n const resolvedApiKey =\n apiKey ??\n process.env.SONOR_API_KEY ??\n ''\n const resolvedApiUrl =\n apiUrl ??\n process.env.SONOR_API_URL ??\n 'https://api.sonor.io'\n\n if (!resolvedApiKey && debug) {\n console.warn(\n '[SiteKitLayout] No API key found. Set SONOR_API_KEY.',\n )\n }\n\n const apiOrigin = resolveApiOrigin(resolvedApiUrl)\n\n // Mint a short-lived client token so the raw key never reaches the browser.\n // Falls back to the raw key if minting is unavailable (older API / transient\n // outage) — the build never breaks. The client refreshes the token as needed.\n const clientCredential = await resolveClientCredential(resolvedApiKey, resolvedApiUrl)\n\n return (\n <>\n <link rel=\"preconnect\" href={apiOrigin} crossOrigin=\"anonymous\" />\n <link rel=\"dns-prefetch\" href={apiOrigin} />\n {/* Server-rendered head components */}\n {favicon && <ManagedFavicon apiKey={resolvedApiKey} apiUrl={resolvedApiUrl} />}\n {managedScripts && <ManagedScripts position=\"head\" />}\n\n {/* Client-side providers island — receives the minted token (or raw-key\n fallback), never the raw key when minting succeeds. */}\n <SiteKitClientProviders\n apiKey={clientCredential}\n apiUrl={resolvedApiUrl}\n projectId={projectId}\n analytics={analytics}\n engage={engage}\n signal={signal}\n sitemapSync={sitemapSync}\n fleet={fleet}\n defer={defer}\n debug={debug}\n >\n {children}\n {showLlmsTxtFooterLink ? (\n <footer className=\"sr-only\" aria-hidden=\"true\">\n <a href=\"/llms.txt\">llms.txt</a>\n </footer>\n ) : null}\n </SiteKitClientProviders>\n\n {/* Server-rendered body-end scripts */}\n {managedScripts && <ManagedScripts position=\"body-end\" />}\n </>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/mint-site-token.ts","../../src/layout/SiteKitLayout.tsx"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @sonordev/site-kit/server — Client-token minting (server-only)\n *\n * SiteKitLayout holds the raw `SONOR_API_KEY` server-side but must NOT ship it to\n * the browser. This mints a short-lived, project-scoped token (server-to-server,\n * authenticated with the raw key, no browser Origin so it passes domain-binding)\n * and returns THAT for the layout to inject instead.\n *\n * `import 'server-only'` makes importing this into a client bundle a hard build\n * error — the raw key stays on the server.\n *\n * Resilience: if minting is unavailable (older API without the endpoint, token\n * auth not configured, transient outage) it returns the RAW KEY, so a build never\n * breaks. Domain-binding is log-only by default, so the raw key still works —\n * worst case is exactly pre-3.2 behavior. Memoized per raw key for the process,\n * so an entire `next build` mints ~once and every static page reuses one seed\n * (the client refreshes on the far side as needed).\n */\nimport 'server-only'\nimport { SITE_KIT_VERSION } from '../shared/version'\n\nconst MINT_PATH = '/api/public/site-token'\nconst MINT_TIMEOUT_MS = 4_000\nconst SITE_TOKEN_PREFIX = 'st1.'\n\ninterface CacheEntry {\n credential: string\n expiresAt: number\n}\nconst cache = new Map<string, CacheEntry>()\n\nexport async function resolveClientCredential(\n rawKey: string,\n apiUrl: string,\n): Promise<string> {\n if (!rawKey) return ''\n // Env should hold a raw key; if it already looks like a token, pass it through.\n if (rawKey.startsWith(SITE_TOKEN_PREFIX)) return rawKey\n\n const now = Date.now()\n const cached = cache.get(rawKey)\n if (cached && cached.expiresAt > now) return cached.credential\n\n const base = (apiUrl || 'https://api.sonor.io').replace(/\\/+$/, '')\n try {\n const res = await fetch(`${base}${MINT_PATH}`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': rawKey,\n 'x-sitekit-version': SITE_KIT_VERSION,\n },\n cache: 'no-store',\n signal:\n typeof AbortSignal !== 'undefined' && AbortSignal.timeout\n ? AbortSignal.timeout(MINT_TIMEOUT_MS)\n : undefined,\n })\n\n if (res.ok) {\n const data: any = await res.json().catch(() => null)\n const token = data?.token\n const expiresIn = typeof data?.expiresIn === 'number' ? data.expiresIn : 3600\n if (typeof token === 'string' && token.startsWith(SITE_TOKEN_PREFIX)) {\n cache.set(rawKey, {\n credential: token,\n expiresAt: now + Math.min((expiresIn * 1000) / 2, 30 * 60_000),\n })\n return token\n }\n } else if (res.status !== 404) {\n // 404 = older API without the endpoint (expected during rollout). Anything\n // else is worth a server-side note. Never logs the key value.\n console.warn(\n `[SiteKitLayout] Token mint returned ${res.status}; injecting raw key for this render.`,\n )\n }\n } catch {\n console.warn('[SiteKitLayout] Token mint unavailable; injecting raw key for this render.')\n }\n\n // Fallback: the raw key (pre-3.2 behavior). Short cache so a transient outage\n // retries soon rather than pinning the raw key for the whole build.\n cache.set(rawKey, { credential: rawKey, expiresAt: now + 60_000 })\n return rawKey\n}\n","/**\n * @sonordev/site-kit/layout — SiteKitLayout\n *\n * The master layout component. A single RSC that auto-composes every\n * site-kit feature your layout needs:\n *\n * Server-side (RSC):\n * - ManagedFavicon (link tags from Portal logo)\n * - ManagedScripts position=\"head\" (tracking pixels, analytics tags)\n * - ManagedScripts position=\"body-end\"\n *\n * Client-side (island):\n * - AnalyticsProvider (page views, scroll depth, heatmap, web vitals)\n * - EngageWidget (popups, nudges, chat)\n * - SignalBridge (A/B experiments, behavior tracking)\n * - SitemapSync (keeps Portal seo_pages in sync)\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { SiteKitLayout } from '@sonordev/site-kit/layout'\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html lang=\"en\">\n * <body>\n * <SiteKitLayout>\n * <Header />\n * <main>{children}</main>\n * <Footer />\n * </SiteKitLayout>\n * </body>\n * </html>\n * )\n * }\n * ```\n *\n * @example Opt out of specific features:\n * ```tsx\n * <SiteKitLayout engage={false} signal={false}>\n * {children}\n * </SiteKitLayout>\n * ```\n */\n\nimport * as React from 'react'\nimport { ManagedFavicon } from '../images/ManagedFavicon'\nimport { ManagedScripts } from '../seo/ManagedScripts'\nimport { resolveApiOrigin } from '../server/server-fetch'\nimport { resolveClientCredential } from '../server/mint-site-token'\nimport { SiteKitClientProviders } from './SiteKitClientProviders'\nimport type { AnalyticsConfig, EngageConfig, SignalConfig } from './types'\n\nexport interface SiteKitLayoutProps {\n children: React.ReactNode\n\n /** Portal API key. Defaults to SONOR_API_KEY env var. */\n apiKey?: string\n /** Portal API URL. Defaults to env or https://api.sonor.io */\n apiUrl?: string\n /** Project ID (for Engage chat room routing). Auto-resolved if not provided. */\n projectId?: string\n\n /** Analytics tracking. true (default) | false | config object */\n analytics?: boolean | AnalyticsConfig\n /** Engage widgets (popups, chat). true (default) | false | config object */\n engage?: boolean | EngageConfig\n /** Signal AI (experiments, behavior). false (default) | true | config object */\n signal?: boolean | SignalConfig\n /** SitemapSync client component. Default: true */\n sitemapSync?: boolean\n /**\n * Fleet heartbeat — reports this site's kit version + active modules to Sonor\n * once per session so the platform can track version drift. Default: true.\n * Deferred like Analytics; never in the hydration path.\n */\n fleet?: boolean\n /**\n * Defer client modules (Analytics, Engage, SitemapSync) until window load +\n * idle or the first user interaction, keeping their JS and mount work out of\n * the critical hydration path. Default: true. Pass false to restore legacy\n * eager mounting. SignalBridge is never deferred.\n */\n defer?: boolean\n /** ManagedFavicon in <head>. Default: true */\n favicon?: boolean\n /** ManagedScripts (head + body-end). Default: true */\n managedScripts?: boolean\n\n /** Debug mode — logs to console. Default: false */\n debug?: boolean\n /**\n * When true, append a screen-reader-only link to `/llms.txt` after main content.\n * Default false — prefer `buildAiDiscoveryHeaders()` for discovery.\n */\n showLlmsTxtFooterLink?: boolean\n}\n\nexport async function SiteKitLayout({\n children,\n apiKey,\n apiUrl,\n projectId,\n analytics = true,\n engage = true,\n signal = false,\n sitemapSync = true,\n fleet = true,\n defer = true,\n favicon = true,\n managedScripts = true,\n debug = false,\n showLlmsTxtFooterLink = false,\n}: SiteKitLayoutProps) {\n // Resolve API config from env (server-side, safe)\n const resolvedApiKey =\n apiKey ??\n process.env.SONOR_API_KEY ??\n ''\n const resolvedApiUrl =\n apiUrl ??\n process.env.SONOR_API_URL ??\n 'https://api.sonor.io'\n\n if (!resolvedApiKey && debug) {\n console.warn(\n '[SiteKitLayout] No API key found. Set SONOR_API_KEY.',\n )\n }\n\n const apiOrigin = resolveApiOrigin(resolvedApiUrl)\n\n // Mint a short-lived client token so the raw key never reaches the browser.\n // Falls back to the raw key if minting is unavailable (older API / transient\n // outage) — the build never breaks. The client refreshes the token as needed.\n const clientCredential = await resolveClientCredential(resolvedApiKey, resolvedApiUrl)\n\n return (\n <>\n <link rel=\"preconnect\" href={apiOrigin} crossOrigin=\"anonymous\" />\n <link rel=\"dns-prefetch\" href={apiOrigin} />\n {/* Server-rendered head components */}\n {favicon && <ManagedFavicon apiKey={resolvedApiKey} apiUrl={resolvedApiUrl} />}\n {managedScripts && <ManagedScripts position=\"head\" />}\n\n {/* Client-side providers island — receives the minted token (or raw-key\n fallback), never the raw key when minting succeeds. */}\n <SiteKitClientProviders\n apiKey={clientCredential}\n apiUrl={resolvedApiUrl}\n projectId={projectId}\n analytics={analytics}\n engage={engage}\n signal={signal}\n sitemapSync={sitemapSync}\n fleet={fleet}\n defer={defer}\n debug={debug}\n >\n {children}\n {showLlmsTxtFooterLink ? (\n <footer className=\"sr-only\" aria-hidden=\"true\">\n <a href=\"/llms.txt\">llms.txt</a>\n </footer>\n ) : null}\n </SiteKitClientProviders>\n\n {/* Server-rendered body-end scripts */}\n {managedScripts && <ManagedScripts position=\"body-end\" />}\n </>\n )\n}\n"]}
|
package/dist/maps/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
5
|
-
require('../chunk-
|
|
4
|
+
var chunkXKV73RDH_js = require('../chunk-XKV73RDH.js');
|
|
5
|
+
require('../chunk-CDETOO5W.js');
|
|
6
6
|
require('../chunk-ZSMWDLMK.js');
|
|
7
7
|
var react = require('react');
|
|
8
8
|
var reactGoogleMaps = require('@vis.gl/react-google-maps');
|
|
@@ -139,7 +139,7 @@ async function fetchMapsConfig() {
|
|
|
139
139
|
const { apiUrl, apiKey } = getApiConfig();
|
|
140
140
|
if (apiKey) {
|
|
141
141
|
try {
|
|
142
|
-
const res = await
|
|
142
|
+
const res = await chunkXKV73RDH_js.sonorFetch(
|
|
143
143
|
`${apiUrl}/api/public/maps/config`,
|
|
144
144
|
{ headers: { "Content-Type": "application/json" } },
|
|
145
145
|
{ apiKey }
|
|
@@ -179,7 +179,7 @@ async function fetchNearbyPlaces(params) {
|
|
|
179
179
|
});
|
|
180
180
|
if (params.radius) searchParams.set("radius", String(params.radius));
|
|
181
181
|
if (params.limit) searchParams.set("limit", String(params.limit));
|
|
182
|
-
const res = await
|
|
182
|
+
const res = await chunkXKV73RDH_js.sonorFetch(
|
|
183
183
|
`${apiUrl}/api/public/maps/nearby-places?${searchParams}`,
|
|
184
184
|
{ headers: { "Content-Type": "application/json" } },
|
|
185
185
|
{ apiKey }
|
package/dist/maps/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { sonorFetch } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
import { sonorFetch } from '../chunk-WS4GNH3F.mjs';
|
|
3
|
+
import '../chunk-YCQHE2XT.mjs';
|
|
4
4
|
import '../chunk-4XPGGLVP.mjs';
|
|
5
5
|
import { useState, useCallback, useEffect } from 'react';
|
|
6
6
|
import { APIProvider, Map as Map$1, AdvancedMarker, Pin, InfoWindow } from '@vis.gl/react-google-maps';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { Metadata } from 'next';
|
|
4
|
+
|
|
5
|
+
interface LandingPageProps {
|
|
6
|
+
/** Campaign identity (e.g. "spring-hvac-2026"). Normalized to a slug; one campaign can back several landing page variants. */
|
|
7
|
+
campaign: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Render the hoisted robots meta tag (default true). Turn off only if the
|
|
11
|
+
* route manages robots itself — landingPageMetadata() remains required
|
|
12
|
+
* either way for reliable noindex.
|
|
13
|
+
*/
|
|
14
|
+
robotsMeta?: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare function LandingPage({ campaign, children, robotsMeta }: LandingPageProps): react_jsx_runtime.JSX.Element;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Metadata for a paid-ad landing page: noindex, nofollow by default.
|
|
20
|
+
*
|
|
21
|
+
* This is the RELIABLE noindex mechanism — export it from the landing
|
|
22
|
+
* route so the robots directives render in <head> at SSR:
|
|
23
|
+
*
|
|
24
|
+
* export const metadata = landingPageMetadata({
|
|
25
|
+
* title: 'Spring HVAC Tune-Up | Acme Heating',
|
|
26
|
+
* description: 'Book your $99 spring tune-up.',
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* The <LandingPage> wrapper also hoists a robots meta tag as belt-and-braces,
|
|
30
|
+
* but body-rendered meta is not honored by all crawlers — always use this
|
|
31
|
+
* helper too. Pass an explicit `robots` object to override the default
|
|
32
|
+
* (e.g. a landing page you deliberately want indexed).
|
|
33
|
+
*/
|
|
34
|
+
declare function landingPageMetadata(meta?: Metadata): Metadata;
|
|
35
|
+
|
|
36
|
+
export { LandingPage as L, type LandingPageProps as a, landingPageMetadata as l };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { Metadata } from 'next';
|
|
4
|
+
|
|
5
|
+
interface LandingPageProps {
|
|
6
|
+
/** Campaign identity (e.g. "spring-hvac-2026"). Normalized to a slug; one campaign can back several landing page variants. */
|
|
7
|
+
campaign: string;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Render the hoisted robots meta tag (default true). Turn off only if the
|
|
11
|
+
* route manages robots itself — landingPageMetadata() remains required
|
|
12
|
+
* either way for reliable noindex.
|
|
13
|
+
*/
|
|
14
|
+
robotsMeta?: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare function LandingPage({ campaign, children, robotsMeta }: LandingPageProps): react_jsx_runtime.JSX.Element;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Metadata for a paid-ad landing page: noindex, nofollow by default.
|
|
20
|
+
*
|
|
21
|
+
* This is the RELIABLE noindex mechanism — export it from the landing
|
|
22
|
+
* route so the robots directives render in <head> at SSR:
|
|
23
|
+
*
|
|
24
|
+
* export const metadata = landingPageMetadata({
|
|
25
|
+
* title: 'Spring HVAC Tune-Up | Acme Heating',
|
|
26
|
+
* description: 'Book your $99 spring tune-up.',
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* The <LandingPage> wrapper also hoists a robots meta tag as belt-and-braces,
|
|
30
|
+
* but body-rendered meta is not honored by all crawlers — always use this
|
|
31
|
+
* helper too. Pass an explicit `robots` object to override the default
|
|
32
|
+
* (e.g. a landing page you deliberately want indexed).
|
|
33
|
+
*/
|
|
34
|
+
declare function landingPageMetadata(meta?: Metadata): Metadata;
|
|
35
|
+
|
|
36
|
+
export { LandingPage as L, type LandingPageProps as a, landingPageMetadata as l };
|
package/dist/middleware/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var chunk3MYZS6PD_js = require('../chunk-3MYZS6PD.js');
|
|
|
4
4
|
var chunk5B4FABFK_js = require('../chunk-5B4FABFK.js');
|
|
5
5
|
var chunkW6K7MQCC_js = require('../chunk-W6K7MQCC.js');
|
|
6
6
|
require('../chunk-JP5WGL75.js');
|
|
7
|
+
var chunkVJMPIRSD_js = require('../chunk-VJMPIRSD.js');
|
|
7
8
|
require('../chunk-ZSMWDLMK.js');
|
|
8
9
|
var server = require('next/server');
|
|
9
10
|
|
|
@@ -117,7 +118,7 @@ function createMiddleware(config) {
|
|
|
117
118
|
const hadVisitorCookie = !!request.cookies.get("_sk_vid")?.value;
|
|
118
119
|
if (!hadVisitorCookie) {
|
|
119
120
|
const maxAgeDays = idCfg.cookieMaxAgeDays ?? 730;
|
|
120
|
-
response.cookies.set("_sk_vid",
|
|
121
|
+
response.cookies.set("_sk_vid", chunkVJMPIRSD_js.uuid(), {
|
|
121
122
|
maxAge: maxAgeDays * 24 * 60 * 60,
|
|
122
123
|
path: "/",
|
|
123
124
|
sameSite: "lax",
|
|
@@ -143,13 +144,6 @@ function createMiddleware(config) {
|
|
|
143
144
|
return response;
|
|
144
145
|
};
|
|
145
146
|
}
|
|
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
|
-
}
|
|
153
147
|
function applyHeaders(response, headers) {
|
|
154
148
|
for (const [key, value] of Object.entries(headers)) {
|
|
155
149
|
response.headers.set(key, value);
|
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/middleware/segment.ts","../../src/middleware/index.ts"],"names":["buildSecurityHeaders","buildAiDiscoveryHeaders","handleManagedRedirects","NextResponse","uuid"],"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// Single source of truth for v4 UUIDs — Web Crypto in the edge runtime, with a\n// non-throwing fallback everywhere else. Do NOT re-fork ID generation here.\nimport { uuid } from '../shared/uuid'\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', uuid(), {\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\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"]}
|
|
@@ -2,6 +2,7 @@ import { buildSecurityHeaders } from '../chunk-6BIPAKL4.mjs';
|
|
|
2
2
|
import { buildAiDiscoveryHeaders } from '../chunk-H4OBGC43.mjs';
|
|
3
3
|
import { handleManagedRedirects } from '../chunk-7LAOWIWE.mjs';
|
|
4
4
|
import '../chunk-B22WSN4L.mjs';
|
|
5
|
+
import { uuid } from '../chunk-6EDVCUAG.mjs';
|
|
5
6
|
import '../chunk-4XPGGLVP.mjs';
|
|
6
7
|
import { NextResponse } from 'next/server';
|
|
7
8
|
|
|
@@ -115,7 +116,7 @@ function createMiddleware(config) {
|
|
|
115
116
|
const hadVisitorCookie = !!request.cookies.get("_sk_vid")?.value;
|
|
116
117
|
if (!hadVisitorCookie) {
|
|
117
118
|
const maxAgeDays = idCfg.cookieMaxAgeDays ?? 730;
|
|
118
|
-
response.cookies.set("_sk_vid",
|
|
119
|
+
response.cookies.set("_sk_vid", uuid(), {
|
|
119
120
|
maxAge: maxAgeDays * 24 * 60 * 60,
|
|
120
121
|
path: "/",
|
|
121
122
|
sameSite: "lax",
|
|
@@ -141,13 +142,6 @@ function createMiddleware(config) {
|
|
|
141
142
|
return response;
|
|
142
143
|
};
|
|
143
144
|
}
|
|
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
|
-
}
|
|
151
145
|
function applyHeaders(response, headers) {
|
|
152
146
|
for (const [key, value] of Object.entries(headers)) {
|
|
153
147
|
response.headers.set(key, value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middleware/segment.ts","../../src/middleware/index.ts"],"names":[],"mappings":";;;;;;;;AAyBA,IAAM,YAAA,GAAe,CAAC,KAAA,EAAO,KAAA,EAAO,QAAQ,YAAA,EAAc,aAAA,EAAe,SAAA,EAAW,KAAA,EAAO,QAAQ,CAAA;AACnG,IAAM,cAAA,GAAiB,CAAC,QAAA,EAAU,cAAA,EAAgB,gBAAgB,IAAI,CAAA;AACtE,IAAM,YAAA,GAAe;AAAA,EACnB,WAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA;AACA,IAAM,YAAA,GAAe,CAAC,SAAA,EAAW,OAAA,EAAS,eAAe,QAAA,EAAU,SAAA,EAAW,QAAA,EAAU,QAAA,EAAU,SAAS,CAAA;AAMpG,SAAS,qBAAA,CACd,SACA,IAAA,EACgB;AAChB,EAAA,MAAM,MAAA,GAAS,QAAQ,OAAA,CAAQ,YAAA;AAC/B,EAAA,MAAM,MAAA,GAAA,CAAU,OAAO,GAAA,CAAI,YAAY,KAAK,EAAA,EAAI,WAAA,GAAc,IAAA,EAAK;AACnE,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,GAAA,CAAI,cAAc,CAAA;AAE1C,EAAA,IAAI,MAAA,GAAwB,QAAA;AAE5B,EAAA,IAAI,YAAA,CAAa,QAAA,CAAS,MAAM,CAAA,EAAG;AACjC,IAAA,MAAA,GAAS,MAAA;AAAA,EACX,CAAA,MAAA,IAAW,cAAA,CAAe,QAAA,CAAS,MAAM,CAAA,EAAG;AAC1C,IAAA,MAAA,GAAS,QAAA;AAAA,EACX,CAAA,MAAA,IAAW,WAAW,SAAA,EAAW;AAC/B,IAAA,MAAA,GAAS,SAAA;AAAA,EACX,CAAA,MAAA,IAAW,WAAW,UAAA,EAAY;AAChC,IAAA,MAAA,GAAS,UAAA;AAAA,EACX,CAAA,MAAA,IAAW,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,IAAK,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA,IAAK,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAA,EAAG;AAElF,IAAA,IAAI,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA,EAAG,MAAA,GAAS,MAAA;AAAA,SAAA,IACzB,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAA,EAAG,MAAA,GAAS,QAAA;AAAA,SACnC,MAAA,GAAS,UAAA;AAAA,EAChB,CAAA,MAAO;AAEL,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA,IAAK,EAAA;AAC9C,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,IAAI;AACF,QAAA,MAAM,OAAO,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,KAAK,WAAA,EAAY;AAC3C,QAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,WAAA,EAAY;AAClD,QAAA,IAAI,IAAA,IAAQ,SAAS,QAAA,EAAU;AAC7B,UAAA,IAAI,YAAA,CAAa,KAAK,CAAC,CAAA,KAAM,KAAK,QAAA,CAAS,CAAC,CAAC,CAAA,EAAG,MAAA,GAAS,SAAA;AAAA,eAAA,IAChD,YAAA,CAAa,KAAK,CAAC,CAAA,KAAM,KAAK,QAAA,CAAS,CAAC,CAAC,CAAA,EAAG,MAAA,GAAS,QAAA;AAAA,eACzD,MAAA,GAAS,UAAA;AAAA,QAChB;AAAA,MACF,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,WAAA,EAAa,IAAA,CAAK,gBAAA,GAAmB,WAAA,GAAc,KAAA;AAAA,IACnD,MAAA;AAAA,IACA,UAAU,QAAA,IAAY;AAAA,GACxB;AACF;AAOO,SAAS,cAAc,GAAA,EAA6B;AACzD,EAAA,MAAM,QAAA,GAAA,CAAY,GAAA,CAAI,QAAA,IAAY,EAAA,EAAI,OAAA,CAAQ,mBAAmB,EAAE,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAChF,EAAA,OAAO,GAAG,GAAA,CAAI,WAAW,IAAI,GAAA,CAAI,MAAM,IAAI,QAAQ,CAAA,CAAA;AACrD;AAGO,SAAS,cAAc,KAAA,EAAyD;AACrF,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,CAAC,WAAA,EAAa,MAAA,EAAQ,QAAQ,CAAA,GAAI,KAAA,CAAM,MAAM,GAAG,CAAA;AACvD,EAAA,IAAI,WAAA,KAAgB,KAAA,IAAS,WAAA,KAAgB,WAAA,EAAa,OAAO,IAAA;AACjE,EAAA,OAAO;AAAA,IACL,WAAA;AAAA,IACA,MAAA,EAAS,CAAC,MAAA,EAAQ,SAAA,EAAW,QAAA,EAAU,UAAA,EAAY,QAAQ,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,GAAI,MAAA,GAAS,QAAA;AAAA,IACzF,UAAU,QAAA,IAAY;AAAA,GACxB;AACF;;;ACAO,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;AAKA,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,QACJ,OAAO,MAAA,CAAO,aAAa,QAAA,GAAW,MAAA,CAAO,WAAW,EAAC;AAC3D,MAAA,MAAM,mBAAmB,CAAC,CAAC,QAAQ,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA,EAAG,KAAA;AAC3D,MAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,QAAA,MAAM,UAAA,GAAa,MAAM,gBAAA,IAAoB,GAAA;AAC7C,QAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,SAAA,EAAW,iBAAA,EAAkB,EAAG;AAAA,UACnD,MAAA,EAAQ,UAAA,GAAa,EAAA,GAAK,EAAA,GAAK,EAAA;AAAA,UAC/B,IAAA,EAAM,GAAA;AAAA,UACN,QAAA,EAAU,KAAA;AAAA,UACV,MAAA,EAAQ;AAAA,SACT,CAAA;AAAA,MACH;AACA,MAAA,IAAI,KAAA,CAAM,YAAY,KAAA,EAAO;AAC3B,QAAA,MAAM,OAAA,GAAU,aAAA;AAAA,UACd,qBAAA,CAAsB,OAAA,EAAS,EAAE,gBAAA,EAAkB;AAAA,SACrD;AACA,QAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,SAAA,EAAW,OAAA,EAAS;AAAA;AAAA,UAEvC,QAAQ,EAAA,GAAK,EAAA;AAAA,UACb,IAAA,EAAM,GAAA;AAAA,UACN,QAAA,EAAU,KAAA;AAAA,UACV,MAAA,EAAQ;AAAA,SACT,CAAA;AAAA,MACH;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;AAMA,SAAS,iBAAA,GAA4B;AACnC,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,UAAA,EAAY,OAAO,OAAO,UAAA,EAAW;AACjF,EAAA,OAAO,sCAAA,CAAuC,OAAA,CAAQ,OAAA,EAAS,CAAC,CAAA,KAAM;AACpE,IAAA,MAAM,CAAA,GAAK,IAAA,CAAK,MAAA,EAAO,GAAI,EAAA,GAAM,CAAA;AACjC,IAAA,OAAA,CAAQ,MAAM,GAAA,GAAM,CAAA,GAAK,IAAI,CAAA,GAAO,CAAA,EAAK,SAAS,EAAE,CAAA;AAAA,EACtD,CAAC,CAAA;AACH;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 — 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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/middleware/segment.ts","../../src/middleware/index.ts"],"names":[],"mappings":";;;;;;;;;AAyBA,IAAM,YAAA,GAAe,CAAC,KAAA,EAAO,KAAA,EAAO,QAAQ,YAAA,EAAc,aAAA,EAAe,SAAA,EAAW,KAAA,EAAO,QAAQ,CAAA;AACnG,IAAM,cAAA,GAAiB,CAAC,QAAA,EAAU,cAAA,EAAgB,gBAAgB,IAAI,CAAA;AACtE,IAAM,YAAA,GAAe;AAAA,EACnB,WAAA;AAAA,EACA,YAAA;AAAA,EACA,WAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EACA,OAAA;AAAA,EACA,YAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA;AACA,IAAM,YAAA,GAAe,CAAC,SAAA,EAAW,OAAA,EAAS,eAAe,QAAA,EAAU,SAAA,EAAW,QAAA,EAAU,QAAA,EAAU,SAAS,CAAA;AAMpG,SAAS,qBAAA,CACd,SACA,IAAA,EACgB;AAChB,EAAA,MAAM,MAAA,GAAS,QAAQ,OAAA,CAAQ,YAAA;AAC/B,EAAA,MAAM,MAAA,GAAA,CAAU,OAAO,GAAA,CAAI,YAAY,KAAK,EAAA,EAAI,WAAA,GAAc,IAAA,EAAK;AACnE,EAAA,MAAM,QAAA,GAAW,MAAA,CAAO,GAAA,CAAI,cAAc,CAAA;AAE1C,EAAA,IAAI,MAAA,GAAwB,QAAA;AAE5B,EAAA,IAAI,YAAA,CAAa,QAAA,CAAS,MAAM,CAAA,EAAG;AACjC,IAAA,MAAA,GAAS,MAAA;AAAA,EACX,CAAA,MAAA,IAAW,cAAA,CAAe,QAAA,CAAS,MAAM,CAAA,EAAG;AAC1C,IAAA,MAAA,GAAS,QAAA;AAAA,EACX,CAAA,MAAA,IAAW,WAAW,SAAA,EAAW;AAC/B,IAAA,MAAA,GAAS,SAAA;AAAA,EACX,CAAA,MAAA,IAAW,WAAW,UAAA,EAAY;AAChC,IAAA,MAAA,GAAS,UAAA;AAAA,EACX,CAAA,MAAA,IAAW,MAAA,CAAO,GAAA,CAAI,YAAY,CAAA,IAAK,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA,IAAK,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAA,EAAG;AAElF,IAAA,IAAI,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA,EAAG,MAAA,GAAS,MAAA;AAAA,SAAA,IACzB,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAA,EAAG,MAAA,GAAS,QAAA;AAAA,SACnC,MAAA,GAAS,UAAA;AAAA,EAChB,CAAA,MAAO;AAEL,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA,IAAK,EAAA;AAC9C,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,IAAI;AACF,QAAA,MAAM,OAAO,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,KAAK,WAAA,EAAY;AAC3C,QAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,WAAA,EAAY;AAClD,QAAA,IAAI,IAAA,IAAQ,SAAS,QAAA,EAAU;AAC7B,UAAA,IAAI,YAAA,CAAa,KAAK,CAAC,CAAA,KAAM,KAAK,QAAA,CAAS,CAAC,CAAC,CAAA,EAAG,MAAA,GAAS,SAAA;AAAA,eAAA,IAChD,YAAA,CAAa,KAAK,CAAC,CAAA,KAAM,KAAK,QAAA,CAAS,CAAC,CAAC,CAAA,EAAG,MAAA,GAAS,QAAA;AAAA,eACzD,MAAA,GAAS,UAAA;AAAA,QAChB;AAAA,MACF,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,WAAA,EAAa,IAAA,CAAK,gBAAA,GAAmB,WAAA,GAAc,KAAA;AAAA,IACnD,MAAA;AAAA,IACA,UAAU,QAAA,IAAY;AAAA,GACxB;AACF;AAOO,SAAS,cAAc,GAAA,EAA6B;AACzD,EAAA,MAAM,QAAA,GAAA,CAAY,GAAA,CAAI,QAAA,IAAY,EAAA,EAAI,OAAA,CAAQ,mBAAmB,EAAE,CAAA,CAAE,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAChF,EAAA,OAAO,GAAG,GAAA,CAAI,WAAW,IAAI,GAAA,CAAI,MAAM,IAAI,QAAQ,CAAA,CAAA;AACrD;AAGO,SAAS,cAAc,KAAA,EAAyD;AACrF,EAAA,IAAI,CAAC,OAAO,OAAO,IAAA;AACnB,EAAA,MAAM,CAAC,WAAA,EAAa,MAAA,EAAQ,QAAQ,CAAA,GAAI,KAAA,CAAM,MAAM,GAAG,CAAA;AACvD,EAAA,IAAI,WAAA,KAAgB,KAAA,IAAS,WAAA,KAAgB,WAAA,EAAa,OAAO,IAAA;AACjE,EAAA,OAAO;AAAA,IACL,WAAA;AAAA,IACA,MAAA,EAAS,CAAC,MAAA,EAAQ,SAAA,EAAW,QAAA,EAAU,UAAA,EAAY,QAAQ,CAAA,CAAE,QAAA,CAAS,MAAM,CAAA,GAAI,MAAA,GAAS,QAAA;AAAA,IACzF,UAAU,QAAA,IAAY;AAAA,GACxB;AACF;;;ACGO,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;AAKA,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,QACJ,OAAO,MAAA,CAAO,aAAa,QAAA,GAAW,MAAA,CAAO,WAAW,EAAC;AAC3D,MAAA,MAAM,mBAAmB,CAAC,CAAC,QAAQ,OAAA,CAAQ,GAAA,CAAI,SAAS,CAAA,EAAG,KAAA;AAC3D,MAAA,IAAI,CAAC,gBAAA,EAAkB;AACrB,QAAA,MAAM,UAAA,GAAa,MAAM,gBAAA,IAAoB,GAAA;AAC7C,QAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,SAAA,EAAW,IAAA,EAAK,EAAG;AAAA,UACtC,MAAA,EAAQ,UAAA,GAAa,EAAA,GAAK,EAAA,GAAK,EAAA;AAAA,UAC/B,IAAA,EAAM,GAAA;AAAA,UACN,QAAA,EAAU,KAAA;AAAA,UACV,MAAA,EAAQ;AAAA,SACT,CAAA;AAAA,MACH;AACA,MAAA,IAAI,KAAA,CAAM,YAAY,KAAA,EAAO;AAC3B,QAAA,MAAM,OAAA,GAAU,aAAA;AAAA,UACd,qBAAA,CAAsB,OAAA,EAAS,EAAE,gBAAA,EAAkB;AAAA,SACrD;AACA,QAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,SAAA,EAAW,OAAA,EAAS;AAAA;AAAA,UAEvC,QAAQ,EAAA,GAAK,EAAA;AAAA,UACb,IAAA,EAAM,GAAA;AAAA,UACN,QAAA,EAAU,KAAA;AAAA,UACV,MAAA,EAAQ;AAAA,SACT,CAAA;AAAA,MACH;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 — 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// Single source of truth for v4 UUIDs — Web Crypto in the edge runtime, with a\n// non-throwing fallback everywhere else. Do NOT re-fork ID generation here.\nimport { uuid } from '../shared/uuid'\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', uuid(), {\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\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"]}
|