@nuxt/scripts 1.0.0-beta.2 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{_ as o,c as s,o as a,a as t,t as r}from"./B8XOar-X.js";import{u as i}from"./DfLgoB--.js";const u={class:"antialiased bg-white dark:bg-[#020420] dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-[#020420] tracking-wide"},l={class:"max-w-520px text-center"},c=["textContent"],d=["textContent"],p=["textContent"],f={__name:"error-500",props:{appName:{type:String,default:"Nuxt"},status:{type:Number,default:500},statusText:{type:String,default:"Internal server error"},description:{type:String,default:"This page is temporarily unavailable."},refresh:{type:String,default:"Refresh this page"}},setup(e){const n=e;return i({title:`${n.status} - ${n.statusText} | ${n.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1,h2{font-size:inherit;font-weight:inherit}h1,h2,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(h,m)=>(a(),s("div",u,[t("div",l,[t("h1",{class:"font-semibold leading-none mb-4 sm:text-[110px] tabular-nums text-[80px]",textContent:r(e.status)},null,8,c),t("h2",{class:"font-semibold mb-2 sm:text-3xl text-2xl",textContent:r(e.statusText)},null,8,d),t("p",{class:"mb-4 px-2 text-[#64748B] text-md",textContent:r(e.description)},null,8,p)])]))}},x=o(f,[["__scopeId","data-v-2367f596"]]);export{x as default};
1
+ import{_ as o,c as s,o as a,a as t,t as r}from"./D-kOnTuH.js";import{u as i}from"./Ds2G8aQM.js";const u={class:"antialiased bg-white dark:bg-[#020420] dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-[#020420] tracking-wide"},l={class:"max-w-520px text-center"},c=["textContent"],d=["textContent"],p=["textContent"],f={__name:"error-500",props:{appName:{type:String,default:"Nuxt"},status:{type:Number,default:500},statusText:{type:String,default:"Internal server error"},description:{type:String,default:"This page is temporarily unavailable."},refresh:{type:String,default:"Refresh this page"}},setup(e){const n=e;return i({title:`${n.status} - ${n.statusText} | ${n.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1,h2{font-size:inherit;font-weight:inherit}h1,h2,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(h,m)=>(a(),s("div",u,[t("div",l,[t("h1",{class:"font-semibold leading-none mb-4 sm:text-[110px] tabular-nums text-[80px]",textContent:r(e.status)},null,8,c),t("h2",{class:"font-semibold mb-2 sm:text-3xl text-2xl",textContent:r(e.statusText)},null,8,d),t("p",{class:"mb-4 px-2 text-[#64748B] text-md",textContent:r(e.description)},null,8,p)])]))}},x=o(f,[["__scopeId","data-v-2367f596"]]);export{x as default};
@@ -1 +1 @@
1
- import{u as a,f as s,h as u,i as r,g as h}from"./B8XOar-X.js";function i(t){const e=t||s();return e.ssrContext?.head||e.runWithContext(()=>{if(u()){const n=r(h);if(!n)throw new Error("[nuxt] [unhead] Missing Unhead instance.");return n}})}function d(t,e={}){const n=e.head||i(e.nuxt);return a(t,{head:n,...e})}export{d as u};
1
+ import{u as a,f as s,h as u,i as r,g as h}from"./D-kOnTuH.js";function i(t){const e=t||s();return e.ssrContext?.head||e.runWithContext(()=>{if(u()){const n=r(h);if(!n)throw new Error("[nuxt] [unhead] Missing Unhead instance.");return n}})}function d(t,e={}){const n=e.head||i(e.nuxt);return a(t,{head:n,...e})}export{d as u};
@@ -1 +1 @@
1
- {"id":"133a46c5-a5c1-4a63-87d1-037947a5bcdb","timestamp":1771947388424}
1
+ {"id":"f1474569-6922-450d-bc3f-4fd5f3e1391a","timestamp":1772019356783}
@@ -0,0 +1 @@
1
+ {"id":"f1474569-6922-450d-bc3f-4fd5f3e1391a","timestamp":1772019356783,"prerendered":[]}
@@ -1 +1 @@
1
- <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/__nuxt-scripts/_nuxt/entry.D45OuV0w.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-scripts/_nuxt/B8XOar-X.js"><script type="module" src="/__nuxt-scripts/_nuxt/B8XOar-X.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{"nuxt-scripts":{version:"",defaultScriptOptions:{trigger:"onNuxtReady"},googleStaticMapsProxy:""},"nuxt-scripts-sw":{path:"/_nuxt-scripts-sw.js"}},app:{baseURL:"/__nuxt-scripts",buildId:"133a46c5-a5c1-4a63-87d1-037947a5bcdb",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1771947392903,false]</script></body></html>
1
+ <!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/__nuxt-scripts/_nuxt/entry.D45OuV0w.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/__nuxt-scripts/_nuxt/D-kOnTuH.js"><script type="module" src="/__nuxt-scripts/_nuxt/D-kOnTuH.js" crossorigin></script></head><body><div id="__nuxt"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{"nuxt-scripts":{version:"",defaultScriptOptions:{trigger:"onNuxtReady"},googleStaticMapsProxy:""},"nuxt-scripts-sw":{path:"/_nuxt-scripts-sw.js"}},app:{baseURL:"/__nuxt-scripts",buildId:"f1474569-6922-450d-bc3f-4fd5f3e1391a",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1772019360972,false]</script></body></html>
package/dist/module.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { FetchOptions } from 'ofetch';
3
3
  import { RegistryScripts, NuxtConfigScriptRegistry, NuxtUseScriptOptionsSerializable, NuxtUseScriptInput } from '../dist/runtime/types.js';
4
+ import { ProxyPrivacyInput } from '../dist/runtime/server/utils/privacy.js';
4
5
 
5
6
  declare module '@nuxt/schema' {
6
7
  interface NuxtHooks {
@@ -8,17 +9,21 @@ declare module '@nuxt/schema' {
8
9
  }
9
10
  }
10
11
  /**
11
- * Privacy mode for first-party proxy requests.
12
+ * Global privacy override for all first-party proxy requests.
12
13
  *
13
- * - `'anonymize'` (default) - Prevents fingerprinting: anonymizes IP addresses to country-level,
14
- * normalizes device info and canvas data. All other data passes through unchanged.
14
+ * By default (`undefined`), each script uses its own privacy controls declared in the registry.
15
+ * Setting this overrides all per-script defaults:
15
16
  *
16
- * - `'proxy'` - Minimal modification: forwards headers and data, but strips sensitive
17
- * auth/session headers (cookie, authorization) to prevent leaking credentials to
18
- * third-party endpoints. Privacy comes from routing requests through your server
19
- * (third parties see server IP, not user IP).
17
+ * - `true` - Full anonymize: anonymizes IP, normalizes User-Agent/language,
18
+ * generalizes screen/hardware/canvas/timezone data.
19
+ *
20
+ * - `false` - Passthrough: forwards headers and data, but strips sensitive
21
+ * auth/session headers (cookie, authorization).
22
+ *
23
+ * - `{ ip: false }` - Selective: override individual flags. Unset flags inherit
24
+ * from the per-script default.
20
25
  */
21
- type FirstPartyPrivacy = 'proxy' | 'anonymize';
26
+ type FirstPartyPrivacy = ProxyPrivacyInput;
22
27
  interface FirstPartyOptions {
23
28
  /**
24
29
  * Path prefix for serving bundled scripts.
@@ -38,14 +43,16 @@ interface FirstPartyOptions {
38
43
  */
39
44
  collectPrefix?: string;
40
45
  /**
41
- * Privacy level for proxied requests.
46
+ * Global privacy override for all proxied scripts.
42
47
  *
43
- * Controls what user information is forwarded to third-party analytics services.
48
+ * By default, each script uses its own privacy controls from the registry.
49
+ * Set this to override all scripts at once:
44
50
  *
45
- * - `'anonymize'` - Prevents fingerprinting by anonymizing IPs and device info (default)
46
- * - `'proxy'` - No modification, just routes through your server
51
+ * - `true` - Full anonymize for all scripts
52
+ * - `false` - Passthrough for all scripts (still strips sensitive auth headers)
53
+ * - `{ ip: false }` - Selective override (unset flags inherit per-script defaults)
47
54
  *
48
- * @default 'anonymize'
55
+ * @default undefined
49
56
  */
50
57
  privacy?: FirstPartyPrivacy;
51
58
  }
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16"
6
6
  },
7
- "version": "1.0.0-beta.2",
7
+ "version": "1.0.0-beta.3",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -126,6 +126,8 @@ function setupPublicAssetStrategy(options = {}) {
126
126
  function buildProxyConfig(collectPrefix) {
127
127
  return {
128
128
  googleAnalytics: {
129
+ // GA4: screen/timezone/UA needed for device, time, and OS reports; rest anonymized safely
130
+ privacy: { ip: true, userAgent: false, language: true, screen: false, timezone: false, hardware: true },
129
131
  rewrite: [
130
132
  // Modern gtag.js uses www.google.com/g/collect
131
133
  { from: "www.google.com/g/collect", to: `${collectPrefix}/ga/g/collect` },
@@ -156,6 +158,8 @@ function buildProxyConfig(collectPrefix) {
156
158
  }
157
159
  },
158
160
  googleTagManager: {
161
+ // GTM: container only, passes data through — downstream tags have their own privacy
162
+ privacy: { ip: false, userAgent: false, language: false, screen: false, timezone: false, hardware: false },
159
163
  rewrite: [
160
164
  { from: "www.googletagmanager.com", to: `${collectPrefix}/gtm` }
161
165
  ],
@@ -164,6 +168,8 @@ function buildProxyConfig(collectPrefix) {
164
168
  }
165
169
  },
166
170
  metaPixel: {
171
+ // Meta: untrusted ad network — full anonymization
172
+ privacy: { ip: true, userAgent: true, language: true, screen: true, timezone: true, hardware: true },
167
173
  rewrite: [
168
174
  // SDK script loading
169
175
  { from: "connect.facebook.net", to: `${collectPrefix}/meta` },
@@ -182,6 +188,8 @@ function buildProxyConfig(collectPrefix) {
182
188
  }
183
189
  },
184
190
  tiktokPixel: {
191
+ // TikTok: untrusted ad network — full anonymization
192
+ privacy: { ip: true, userAgent: true, language: true, screen: true, timezone: true, hardware: true },
185
193
  rewrite: [
186
194
  { from: "analytics.tiktok.com", to: `${collectPrefix}/tiktok` }
187
195
  ],
@@ -190,6 +198,8 @@ function buildProxyConfig(collectPrefix) {
190
198
  }
191
199
  },
192
200
  segment: {
201
+ // Segment: trusted data pipeline — needs maximum fidelity for downstream destinations
202
+ privacy: { ip: false, userAgent: false, language: false, screen: false, timezone: false, hardware: false },
193
203
  rewrite: [
194
204
  { from: "api.segment.io", to: `${collectPrefix}/segment` },
195
205
  { from: "cdn.segment.com", to: `${collectPrefix}/segment-cdn` }
@@ -200,6 +210,8 @@ function buildProxyConfig(collectPrefix) {
200
210
  }
201
211
  },
202
212
  xPixel: {
213
+ // X/Twitter: untrusted ad network — full anonymization
214
+ privacy: { ip: true, userAgent: true, language: true, screen: true, timezone: true, hardware: true },
203
215
  rewrite: [
204
216
  { from: "analytics.twitter.com", to: `${collectPrefix}/x` },
205
217
  { from: "t.co", to: `${collectPrefix}/x-t` }
@@ -210,6 +222,8 @@ function buildProxyConfig(collectPrefix) {
210
222
  }
211
223
  },
212
224
  snapchatPixel: {
225
+ // Snapchat: untrusted ad network — full anonymization
226
+ privacy: { ip: true, userAgent: true, language: true, screen: true, timezone: true, hardware: true },
213
227
  rewrite: [
214
228
  { from: "tr.snapchat.com", to: `${collectPrefix}/snap` }
215
229
  ],
@@ -218,6 +232,8 @@ function buildProxyConfig(collectPrefix) {
218
232
  }
219
233
  },
220
234
  redditPixel: {
235
+ // Reddit: untrusted ad network — full anonymization
236
+ privacy: { ip: true, userAgent: true, language: true, screen: true, timezone: true, hardware: true },
221
237
  rewrite: [
222
238
  { from: "alb.reddit.com", to: `${collectPrefix}/reddit` }
223
239
  ],
@@ -226,6 +242,8 @@ function buildProxyConfig(collectPrefix) {
226
242
  }
227
243
  },
228
244
  clarity: {
245
+ // Clarity: screen/UA/timezone needed for heatmaps and device filtering; rest anonymized
246
+ privacy: { ip: true, userAgent: false, language: true, screen: false, timezone: false, hardware: true },
229
247
  rewrite: [
230
248
  // Main clarity domain
231
249
  { from: "www.clarity.ms", to: `${collectPrefix}/clarity` },
@@ -243,7 +261,22 @@ function buildProxyConfig(collectPrefix) {
243
261
  [`${collectPrefix}/clarity-events/**`]: { proxy: "https://e.clarity.ms/**" }
244
262
  }
245
263
  },
264
+ posthog: {
265
+ // No rewrites needed - PostHog uses NPM mode, SDK URLs are set via api_host config
266
+ // PostHog: needs real IP for GeoIP enrichment + feature flag targeting
267
+ privacy: { ip: false, userAgent: false, language: false, screen: false, timezone: false, hardware: false },
268
+ routes: {
269
+ // US region
270
+ [`${collectPrefix}/ph/static/**`]: { proxy: "https://us-assets.i.posthog.com/static/**" },
271
+ [`${collectPrefix}/ph/**`]: { proxy: "https://us.i.posthog.com/**" },
272
+ // EU region
273
+ [`${collectPrefix}/ph-eu/static/**`]: { proxy: "https://eu-assets.i.posthog.com/static/**" },
274
+ [`${collectPrefix}/ph-eu/**`]: { proxy: "https://eu.i.posthog.com/**" }
275
+ }
276
+ },
246
277
  hotjar: {
278
+ // Hotjar: screen/UA/timezone needed for heatmaps and device segmentation; rest anonymized
279
+ privacy: { ip: true, userAgent: false, language: true, screen: false, timezone: false, hardware: true },
247
280
  rewrite: [
248
281
  // Static assets
249
282
  { from: "static.hotjar.com", to: `${collectPrefix}/hotjar` },
@@ -1037,7 +1070,7 @@ const module$1 = defineNuxtModule({
1037
1070
  const firstPartyEnabled = !!config.firstParty;
1038
1071
  const firstPartyPrefix = typeof config.firstParty === "object" ? config.firstParty.prefix : void 0;
1039
1072
  const firstPartyCollectPrefix = typeof config.firstParty === "object" ? config.firstParty.collectPrefix || "/_proxy" : "/_proxy";
1040
- const firstPartyPrivacy = typeof config.firstParty === "object" ? config.firstParty.privacy ?? "anonymize" : "anonymize";
1073
+ const firstPartyPrivacy = typeof config.firstParty === "object" ? config.firstParty.privacy : void 0;
1041
1074
  const assetsPrefix = firstPartyPrefix || config.assets?.prefix || "/_scripts";
1042
1075
  if (config.partytown?.length) {
1043
1076
  config.registry = config.registry || {};
@@ -1225,6 +1258,7 @@ export default defineNuxtPlugin({
1225
1258
  const proxyConfigs = getAllProxyConfigs(firstPartyCollectPrefix);
1226
1259
  const registryKeys = Object.keys(config.registry || {});
1227
1260
  const neededRoutes = {};
1261
+ const routePrivacyOverrides = {};
1228
1262
  const unsupportedScripts = [];
1229
1263
  for (const key of registryKeys) {
1230
1264
  const script = registryScriptsWithImport.find((s) => s.import.name.toLowerCase() === `usescript${key.toLowerCase()}`);
@@ -1233,11 +1267,21 @@ export default defineNuxtPlugin({
1233
1267
  const proxyConfig = proxyConfigs[proxyKey];
1234
1268
  if (proxyConfig?.routes) {
1235
1269
  Object.assign(neededRoutes, proxyConfig.routes);
1270
+ for (const routePath of Object.keys(proxyConfig.routes)) {
1271
+ routePrivacyOverrides[routePath] = proxyConfig.privacy;
1272
+ }
1236
1273
  } else {
1237
1274
  unsupportedScripts.push(key);
1238
1275
  }
1239
1276
  }
1240
1277
  }
1278
+ if (config.registry?.posthog && typeof config.registry.posthog === "object") {
1279
+ const phConfig = config.registry.posthog;
1280
+ if (!phConfig.apiHost) {
1281
+ const region = phConfig.region || "us";
1282
+ phConfig.apiHost = region === "eu" ? `${firstPartyCollectPrefix}/ph-eu` : `${firstPartyCollectPrefix}/ph`;
1283
+ }
1284
+ }
1241
1285
  if (unsupportedScripts.length && nuxt.options.dev) {
1242
1286
  logger.warn(
1243
1287
  `First-party mode is enabled but these scripts don't support it yet: ${unsupportedScripts.join(", ")}.
@@ -1262,13 +1306,17 @@ They will load directly from third-party servers. Request support at https://git
1262
1306
  nuxt.options.runtimeConfig["nuxt-scripts-proxy"] = {
1263
1307
  routes: flatRoutes,
1264
1308
  privacy: firstPartyPrivacy,
1309
+ // undefined = use per-script defaults, set = global override
1310
+ routePrivacy: routePrivacyOverrides,
1311
+ // per-script privacy from registry
1265
1312
  rewrites: allRewrites
1266
1313
  };
1267
1314
  if (Object.keys(neededRoutes).length) {
1268
1315
  if (nuxt.options.dev) {
1269
1316
  const routeCount = Object.keys(neededRoutes).length;
1270
1317
  const scriptsCount = registryKeys.length;
1271
- logger.success(`First-party mode enabled for ${scriptsCount} script(s), ${routeCount} proxy route(s) configured (privacy: ${firstPartyPrivacy})`);
1318
+ const privacyLabel = firstPartyPrivacy === void 0 ? "per-script" : typeof firstPartyPrivacy === "boolean" ? firstPartyPrivacy ? "anonymize" : "passthrough" : "custom";
1319
+ logger.success(`First-party mode enabled for ${scriptsCount} script(s), ${routeCount} proxy route(s) configured (privacy: ${privacyLabel})`);
1272
1320
  if (logger.level >= 4) {
1273
1321
  for (const [path, config2] of Object.entries(neededRoutes)) {
1274
1322
  logger.debug(` ${path} \u2192 ${config2.proxy}`);
package/dist/registry.mjs CHANGED
@@ -29,6 +29,7 @@ async function registry(resolve) {
29
29
  {
30
30
  label: "PostHog",
31
31
  src: false,
32
+ proxy: "posthog",
32
33
  scriptBundling: false,
33
34
  category: "analytics",
34
35
  logo: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 128 128"><path fill="#1d4aff" d="M0 .52v32.15l31.79 31.78V32.3L0 .52zm32.3 32.15v32.15l31.78 31.78V64.45L32.3 32.67zM0 64.97v32.15l31.79 31.78V96.75L0 64.97zm64.6-32.3v32.15l31.78 31.78V64.45L64.6 32.67zm31.78 31.78v32.15l31.78 31.78V96.23l-31.78-31.78zm-64.08.52v32.15l31.78 31.78V96.75L32.3 64.97zM64.6 .52v32.15l31.78 31.78V32.3L64.6 .52zm0 64.45v32.15l31.78 31.78V96.75L64.6 64.97z"/></svg>`,
@@ -3,6 +3,7 @@ import type { RegistryScriptInput } from '#nuxt-scripts/types';
3
3
  export declare const PostHogOptions: import("valibot").ObjectSchema<{
4
4
  readonly apiKey: import("valibot").StringSchema<undefined>;
5
5
  readonly region: import("valibot").OptionalSchema<import("valibot").UnionSchema<[import("valibot").LiteralSchema<"us", undefined>, import("valibot").LiteralSchema<"eu", undefined>], undefined>, undefined>;
6
+ readonly apiHost: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
6
7
  readonly autocapture: import("valibot").OptionalSchema<import("valibot").BooleanSchema<undefined>, undefined>;
7
8
  readonly capturePageview: import("valibot").OptionalSchema<import("valibot").BooleanSchema<undefined>, undefined>;
8
9
  readonly capturePageleave: import("valibot").OptionalSchema<import("valibot").BooleanSchema<undefined>, undefined>;
@@ -4,6 +4,7 @@ import { logger } from "../logger.js";
4
4
  export const PostHogOptions = object({
5
5
  apiKey: string(),
6
6
  region: optional(union([literal("us"), literal("eu")])),
7
+ apiHost: optional(string()),
7
8
  autocapture: optional(boolean()),
8
9
  capturePageview: optional(boolean()),
9
10
  capturePageleave: optional(boolean()),
@@ -47,10 +48,8 @@ export function useScriptPostHog(_options) {
47
48
  return;
48
49
  }
49
50
  const region = options?.region || "us";
50
- const apiHost = region === "eu" ? "https://eu.i.posthog.com" : "https://us.i.posthog.com";
51
- console.log("[PostHog] Starting dynamic import of posthog-js...");
51
+ const apiHost = options?.apiHost || (region === "eu" ? "https://eu.i.posthog.com" : "https://us.i.posthog.com");
52
52
  window.__posthogInitPromise = import("posthog-js").then(({ default: posthog }) => {
53
- console.log("[PostHog] posthog-js imported successfully");
54
53
  const config = {
55
54
  api_host: apiHost,
56
55
  ...options?.config
@@ -63,25 +62,20 @@ export function useScriptPostHog(_options) {
63
62
  config.capture_pageleave = options.capturePageleave;
64
63
  if (typeof options?.disableSessionRecording === "boolean")
65
64
  config.disable_session_recording = options.disableSessionRecording;
66
- console.log("[PostHog] Calling posthog.init with apiKey:", options.apiKey, "config:", config);
67
65
  const instance = posthog.init(options.apiKey, config);
68
66
  if (!instance) {
69
67
  logger.error("PostHog init returned undefined - initialization failed");
70
68
  delete window._posthogQueue;
71
69
  return void 0;
72
70
  }
73
- console.log("[PostHog] posthog.init succeeded, instance:", instance);
74
71
  window.posthog = instance;
75
72
  if (window._posthogQueue && window._posthogQueue.length > 0) {
76
- console.log("[PostHog] Flushing", window._posthogQueue.length, "queued calls");
77
73
  window._posthogQueue.forEach((q) => window.posthog[q.prop]?.(...q.args));
78
74
  delete window._posthogQueue;
79
75
  }
80
- console.log("[PostHog] Initialization complete!");
81
76
  return window.posthog;
82
77
  }).catch((e) => {
83
78
  logger.error("Failed to load posthog-js:", e);
84
- console.error("[PostHog] Import/init error:", e);
85
79
  delete window._posthogQueue;
86
80
  return void 0;
87
81
  });
@@ -4,20 +4,20 @@ import { useStorage, useNitroApp } from "nitropack/runtime";
4
4
  import { hash } from "ohash";
5
5
  import { rewriteScriptUrls } from "../utils/pure.js";
6
6
  import {
7
- FINGERPRINT_HEADERS,
8
- IP_HEADERS,
9
7
  SENSITIVE_HEADERS,
10
8
  anonymizeIP,
11
9
  normalizeLanguage,
12
10
  normalizeUserAgent,
13
- stripPayloadFingerprinting
11
+ stripPayloadFingerprinting,
12
+ resolvePrivacy,
13
+ mergePrivacy
14
14
  } from "./utils/privacy.js";
15
- function stripQueryFingerprinting(query) {
16
- const stripped = stripPayloadFingerprinting(query);
15
+ function stripQueryFingerprinting(query, privacy) {
16
+ const stripped = stripPayloadFingerprinting(query, privacy);
17
17
  const params = new URLSearchParams();
18
18
  for (const [key, value] of Object.entries(stripped)) {
19
19
  if (value !== void 0 && value !== null) {
20
- params.set(key, String(value));
20
+ params.set(key, typeof value === "object" ? JSON.stringify(value) : String(value));
21
21
  }
22
22
  }
23
23
  return params.toString();
@@ -32,7 +32,7 @@ export default defineEventHandler(async (event) => {
32
32
  statusMessage: "First-party proxy not configured"
33
33
  });
34
34
  }
35
- const { routes, privacy, cacheTtl = 3600, debug = import.meta.dev } = proxyConfig;
35
+ const { routes, privacy: globalPrivacy, routePrivacy, cacheTtl = 3600, debug = import.meta.dev } = proxyConfig;
36
36
  const path = event.path;
37
37
  const log = debug ? (message, ...args) => {
38
38
  console.debug(message, ...args);
@@ -40,17 +40,19 @@ export default defineEventHandler(async (event) => {
40
40
  };
41
41
  let targetBase;
42
42
  let matchedPrefix;
43
+ let matchedRoutePattern;
43
44
  const sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
44
45
  for (const [routePattern, target] of sortedRoutes) {
45
46
  const prefix = routePattern.replace(/\/\*\*$/, "");
46
47
  if (path.startsWith(prefix)) {
47
48
  targetBase = target.replace(/\/\*\*$/, "");
48
49
  matchedPrefix = prefix;
50
+ matchedRoutePattern = routePattern;
49
51
  log("[proxy] Matched:", prefix, "->", targetBase);
50
52
  break;
51
53
  }
52
54
  }
53
- if (!targetBase || !matchedPrefix) {
55
+ if (!targetBase || !matchedPrefix || !matchedRoutePattern) {
54
56
  log("[proxy] No match for path:", path);
55
57
  throw createError({
56
58
  statusCode: 404,
@@ -58,54 +60,68 @@ export default defineEventHandler(async (event) => {
58
60
  message: `No proxy target found for path: ${path}`
59
61
  });
60
62
  }
63
+ const perScriptInput = routePrivacy[matchedRoutePattern];
64
+ if (debug && perScriptInput === void 0) {
65
+ log("[proxy] WARNING: No privacy config for route", matchedRoutePattern, "\u2014 defaulting to full anonymization");
66
+ }
67
+ const perScriptResolved = resolvePrivacy(perScriptInput ?? true);
68
+ const privacy = globalPrivacy !== void 0 ? mergePrivacy(perScriptResolved, globalPrivacy) : perScriptResolved;
69
+ const anyPrivacy = privacy.ip || privacy.userAgent || privacy.language || privacy.screen || privacy.timezone || privacy.hardware;
61
70
  let targetPath = path.slice(matchedPrefix.length);
62
71
  if (targetPath && !targetPath.startsWith("/")) {
63
72
  targetPath = "/" + targetPath;
64
73
  }
65
74
  let targetUrl = targetBase + targetPath;
66
- if (privacy === "anonymize") {
75
+ if (anyPrivacy) {
67
76
  const query = getQuery(event);
68
77
  if (Object.keys(query).length > 0) {
69
- const strippedQuery = stripQueryFingerprinting(query);
78
+ const strippedQuery = stripQueryFingerprinting(query, privacy);
70
79
  const basePath = targetUrl.split("?")[0] || targetUrl;
71
80
  targetUrl = strippedQuery ? `${basePath}?${strippedQuery}` : basePath;
72
81
  }
73
82
  }
74
83
  const originalHeaders = getHeaders(event);
75
84
  const headers = {};
76
- if (privacy === "proxy") {
77
- for (const [key, value] of Object.entries(originalHeaders)) {
78
- if (!value) continue;
79
- if (SENSITIVE_HEADERS.includes(key.toLowerCase())) continue;
80
- headers[key] = value;
85
+ for (const [key, value] of Object.entries(originalHeaders)) {
86
+ if (!value) continue;
87
+ const lowerKey = key.toLowerCase();
88
+ if (SENSITIVE_HEADERS.includes(lowerKey)) continue;
89
+ if (anyPrivacy && lowerKey === "content-length") continue;
90
+ if (lowerKey === "x-forwarded-for" || lowerKey === "x-real-ip" || lowerKey === "forwarded" || lowerKey === "cf-connecting-ip" || lowerKey === "true-client-ip" || lowerKey === "x-client-ip" || lowerKey === "x-cluster-client-ip") {
91
+ if (privacy.ip) continue;
92
+ headers[lowerKey] = value;
93
+ continue;
81
94
  }
82
- } else {
83
- for (const [key, value] of Object.entries(originalHeaders)) {
84
- if (!value)
85
- continue;
86
- const lowerKey = key.toLowerCase();
87
- if (IP_HEADERS.includes(lowerKey))
88
- continue;
89
- if (SENSITIVE_HEADERS.includes(lowerKey))
90
- continue;
91
- if (lowerKey === "content-length")
92
- continue;
93
- if (lowerKey === "user-agent") {
94
- headers[key] = normalizeUserAgent(value);
95
- } else if (lowerKey === "accept-language") {
96
- headers[key] = normalizeLanguage(value);
97
- } else if (lowerKey === "sec-ch-ua" || lowerKey === "sec-ch-ua-full-version-list") {
98
- headers[key] = value.replace(/;v="(\d+)\.[^"]*"/g, ';v="$1"');
99
- } else if (FINGERPRINT_HEADERS.includes(lowerKey)) {
100
- headers[key] = value;
101
- } else {
102
- headers[key] = value;
103
- }
95
+ if (lowerKey === "user-agent") {
96
+ headers[key] = privacy.userAgent ? normalizeUserAgent(value) : value;
97
+ continue;
98
+ }
99
+ if (lowerKey === "accept-language") {
100
+ headers[key] = privacy.language ? normalizeLanguage(value) : value;
101
+ continue;
102
+ }
103
+ if (lowerKey === "sec-ch-ua" || lowerKey === "sec-ch-ua-full-version-list") {
104
+ headers[lowerKey] = privacy.hardware ? value.replace(/;v="(\d+)\.[^"]*"/g, ';v="$1"') : value;
105
+ continue;
104
106
  }
107
+ if (lowerKey === "sec-ch-ua-platform-version" || lowerKey === "sec-ch-ua-arch" || lowerKey === "sec-ch-ua-model" || lowerKey === "sec-ch-ua-bitness") {
108
+ if (privacy.hardware) continue;
109
+ headers[lowerKey] = value;
110
+ continue;
111
+ }
112
+ headers[key] = value;
113
+ }
114
+ if (!headers["x-forwarded-for"]) {
105
115
  const clientIP = getRequestIP(event, { xForwardedFor: true });
106
116
  if (clientIP) {
107
- headers["x-forwarded-for"] = anonymizeIP(clientIP);
117
+ if (privacy.ip) {
118
+ headers["x-forwarded-for"] = anonymizeIP(clientIP);
119
+ } else {
120
+ headers["x-forwarded-for"] = clientIP;
121
+ }
108
122
  }
123
+ } else if (privacy.ip) {
124
+ headers["x-forwarded-for"] = headers["x-forwarded-for"].split(",").map((ip) => anonymizeIP(ip.trim())).join(", ");
109
125
  }
110
126
  let body;
111
127
  let rawBody;
@@ -114,9 +130,9 @@ export default defineEventHandler(async (event) => {
114
130
  const originalQuery = getQuery(event);
115
131
  if (method === "POST" || method === "PUT" || method === "PATCH") {
116
132
  rawBody = await readBody(event);
117
- if (privacy === "anonymize" && rawBody) {
133
+ if (anyPrivacy && rawBody) {
118
134
  if (typeof rawBody === "object") {
119
- body = stripPayloadFingerprinting(rawBody);
135
+ body = stripPayloadFingerprinting(rawBody, privacy);
120
136
  } else if (typeof rawBody === "string") {
121
137
  if (rawBody.startsWith("{") || rawBody.startsWith("[")) {
122
138
  let parsed = null;
@@ -125,7 +141,7 @@ export default defineEventHandler(async (event) => {
125
141
  } catch {
126
142
  }
127
143
  if (parsed && typeof parsed === "object") {
128
- body = stripPayloadFingerprinting(parsed);
144
+ body = stripPayloadFingerprinting(parsed, privacy);
129
145
  } else {
130
146
  body = rawBody;
131
147
  }
@@ -135,7 +151,7 @@ export default defineEventHandler(async (event) => {
135
151
  params.forEach((value, key) => {
136
152
  obj[key] = value;
137
153
  });
138
- const stripped = stripPayloadFingerprinting(obj);
154
+ const stripped = stripPayloadFingerprinting(obj, privacy);
139
155
  const stringified = {};
140
156
  for (const [k, v] of Object.entries(stripped)) {
141
157
  if (v === void 0 || v === null) continue;
@@ -165,7 +181,7 @@ export default defineEventHandler(async (event) => {
165
181
  },
166
182
  stripped: {
167
183
  headers,
168
- query: privacy === "anonymize" ? stripPayloadFingerprinting(originalQuery) : originalQuery,
184
+ query: anyPrivacy ? stripPayloadFingerprinting(originalQuery, privacy) : originalQuery,
169
185
  body: body ?? null
170
186
  }
171
187
  });
@@ -1,3 +1,44 @@
1
+ /**
2
+ * Granular privacy controls for the first-party proxy.
3
+ * Each flag controls both headers AND body/query params for its domain.
4
+ */
5
+ export interface ProxyPrivacy {
6
+ /** Anonymize IP (headers + body). When false, real IP is forwarded via x-forwarded-for. */
7
+ ip?: boolean;
8
+ /** Normalize User-Agent (headers + body) */
9
+ userAgent?: boolean;
10
+ /** Normalize Accept-Language (headers + body) */
11
+ language?: boolean;
12
+ /** Generalize screen resolution, viewport, hardware concurrency, device memory */
13
+ screen?: boolean;
14
+ /** Generalize timezone offset and IANA timezone names */
15
+ timezone?: boolean;
16
+ /** Anonymize hardware fingerprints: canvas/webgl/audio, plugins/fonts, browser versions, device info */
17
+ hardware?: boolean;
18
+ }
19
+ /**
20
+ * Privacy input: `true` = full anonymize, `false` = passthrough (still strips sensitive headers),
21
+ * or a `ProxyPrivacy` object for granular control (unset flags default to `false` — opt-in).
22
+ */
23
+ export type ProxyPrivacyInput = boolean | ProxyPrivacy | null;
24
+ /** Resolved privacy with all flags explicitly set. */
25
+ export type ResolvedProxyPrivacy = Required<ProxyPrivacy>;
26
+ /**
27
+ * Normalize a privacy input to a fully-resolved object.
28
+ * Privacy is opt-in: unset object flags default to `false`.
29
+ * Each script in the registry explicitly sets all flags for its needs.
30
+ * - `true` → all flags true (full anonymize)
31
+ * - `false` / `undefined` → all flags false (passthrough)
32
+ * - `{ ip: true, hardware: true }` → only those active, rest off
33
+ */
34
+ export declare function resolvePrivacy(input?: ProxyPrivacyInput): ResolvedProxyPrivacy;
35
+ /**
36
+ * Merge privacy settings: `override` fields take precedence over `base` field-by-field.
37
+ * When `override` is undefined, returns `base` unchanged.
38
+ * When `override` is a boolean, it fully replaces `base`.
39
+ * When `override` is an object, only explicitly-set fields override.
40
+ */
41
+ export declare function mergePrivacy(base: ResolvedProxyPrivacy, override?: ProxyPrivacyInput): ResolvedProxyPrivacy;
1
42
  /**
2
43
  * Headers that reveal user IP address - stripped in proxy mode,
3
44
  * anonymized in anonymize mode.
@@ -93,5 +134,8 @@ export declare function anonymizeDeviceInfo(value: string): string;
93
134
  * Recursively anonymize fingerprinting data in payload.
94
135
  * Fields are generalized or normalized rather than stripped, so endpoints
95
136
  * still receive valid data with reduced fingerprinting precision.
137
+ *
138
+ * When `privacy` is provided, only categories with their flag set to `true` are processed.
139
+ * Default (no arg) = all categories active, so existing callers work unchanged.
96
140
  */
97
- export declare function stripPayloadFingerprinting(payload: Record<string, unknown>): Record<string, unknown>;
141
+ export declare function stripPayloadFingerprinting(payload: Record<string, unknown>, privacy?: ResolvedProxyPrivacy): Record<string, unknown>;