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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +3 -3
  2. package/dist/client/200.html +1 -1
  3. package/dist/client/404.html +1 -1
  4. package/dist/client/_nuxt/{DvH517bE.js → BlmrFwhD.js} +1 -1
  5. package/dist/client/_nuxt/{DfLgoB--.js → BwCYQWJt.js} +1 -1
  6. package/dist/client/_nuxt/DvbTvDd0.js +162 -0
  7. package/dist/client/_nuxt/{B66N9HCo.js → ZrewjUYk.js} +1 -1
  8. package/dist/client/_nuxt/builds/latest.json +1 -1
  9. package/dist/client/_nuxt/builds/meta/6660a023-888d-415f-b66d-ce774e6f8f11.json +1 -0
  10. package/dist/client/_nuxt/entry.CACgbLJl.css +1 -0
  11. package/dist/client/_nuxt/error-404.CHeaW3dp.css +1 -0
  12. package/dist/client/_nuxt/error-500.DvOvWme_.css +1 -0
  13. package/dist/client/index.html +1 -1
  14. package/dist/module.d.mts +27 -18
  15. package/dist/module.d.ts +178 -0
  16. package/dist/module.json +1 -1
  17. package/dist/module.mjs +763 -526
  18. package/dist/registry.d.ts +6 -0
  19. package/dist/registry.mjs +109 -21
  20. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.d.vue.ts +2 -2
  21. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +7 -7
  22. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue.d.ts +2 -2
  23. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue +6 -6
  24. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue +7 -7
  25. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsHeatmapLayer.vue +6 -6
  26. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsInfoWindow.vue +12 -12
  27. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarker.vue +6 -6
  28. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.d.vue.ts +1 -1
  29. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.vue +6 -6
  30. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.vue.d.ts +1 -1
  31. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPinElement.vue +5 -5
  32. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPolygon.vue +7 -7
  33. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPolyline.vue +7 -7
  34. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsRectangle.vue +7 -7
  35. package/dist/runtime/components/ScriptCrisp.vue +1 -1
  36. package/dist/runtime/components/ScriptGoogleAdsense.vue +1 -1
  37. package/dist/runtime/components/ScriptGravatar.d.vue.ts +22 -0
  38. package/dist/runtime/components/ScriptGravatar.vue +46 -0
  39. package/dist/runtime/components/ScriptGravatar.vue.d.ts +22 -0
  40. package/dist/runtime/components/ScriptInstagramEmbed.vue +1 -1
  41. package/dist/runtime/components/ScriptIntercom.vue +4 -3
  42. package/dist/runtime/components/ScriptPayPalButtons.d.vue.ts +43 -32
  43. package/dist/runtime/components/ScriptPayPalButtons.vue +48 -79
  44. package/dist/runtime/components/ScriptPayPalButtons.vue.d.ts +43 -32
  45. package/dist/runtime/components/ScriptPayPalMessages.d.vue.ts +37 -23
  46. package/dist/runtime/components/ScriptPayPalMessages.vue +46 -50
  47. package/dist/runtime/components/ScriptPayPalMessages.vue.d.ts +37 -23
  48. package/dist/runtime/components/ScriptStripePricingTable.vue +2 -2
  49. package/dist/runtime/components/ScriptVimeoPlayer.d.vue.ts +9 -0
  50. package/dist/runtime/components/ScriptVimeoPlayer.vue +13 -10
  51. package/dist/runtime/components/ScriptVimeoPlayer.vue.d.ts +9 -0
  52. package/dist/runtime/components/ScriptXEmbed.d.vue.ts +1 -1
  53. package/dist/runtime/components/ScriptXEmbed.vue +1 -1
  54. package/dist/runtime/components/ScriptXEmbed.vue.d.ts +1 -1
  55. package/dist/runtime/components/ScriptYouTubePlayer.d.vue.ts +2 -2
  56. package/dist/runtime/components/ScriptYouTubePlayer.vue +11 -5
  57. package/dist/runtime/components/ScriptYouTubePlayer.vue.d.ts +2 -2
  58. package/dist/runtime/composables/useScript.js +11 -6
  59. package/dist/runtime/composables/useScriptEventPage.js +2 -2
  60. package/dist/runtime/composables/useScriptTriggerConsent.js +1 -1
  61. package/dist/runtime/composables/useScriptTriggerElement.js +1 -1
  62. package/dist/runtime/composables/useScriptTriggerIdleTimeout.js +1 -1
  63. package/dist/runtime/registry/clarity.d.ts +10 -15
  64. package/dist/runtime/registry/clarity.js +22 -31
  65. package/dist/runtime/registry/cloudflare-web-analytics.d.ts +2 -13
  66. package/dist/runtime/registry/cloudflare-web-analytics.js +2 -14
  67. package/dist/runtime/registry/crisp.d.ts +9 -39
  68. package/dist/runtime/registry/crisp.js +2 -33
  69. package/dist/runtime/registry/databuddy-analytics.d.ts +2 -35
  70. package/dist/runtime/registry/databuddy-analytics.js +20 -45
  71. package/dist/runtime/registry/fathom-analytics.d.ts +6 -25
  72. package/dist/runtime/registry/fathom-analytics.js +2 -24
  73. package/dist/runtime/registry/google-adsense.d.ts +2 -10
  74. package/dist/runtime/registry/google-adsense.js +2 -11
  75. package/dist/runtime/registry/google-analytics.d.ts +3 -5
  76. package/dist/runtime/registry/google-analytics.js +3 -8
  77. package/dist/runtime/registry/google-maps.d.ts +3 -9
  78. package/dist/runtime/registry/google-maps.js +2 -8
  79. package/dist/runtime/registry/google-recaptcha.d.ts +2 -6
  80. package/dist/runtime/registry/google-recaptcha.js +4 -12
  81. package/dist/runtime/registry/google-sign-in.d.ts +2 -13
  82. package/dist/runtime/registry/google-sign-in.js +2 -22
  83. package/dist/runtime/registry/google-tag-manager.d.ts +3 -28
  84. package/dist/runtime/registry/google-tag-manager.js +4 -27
  85. package/dist/runtime/registry/gravatar.d.ts +25 -0
  86. package/dist/runtime/registry/gravatar.js +32 -0
  87. package/dist/runtime/registry/hotjar.d.ts +3 -5
  88. package/dist/runtime/registry/hotjar.js +2 -5
  89. package/dist/runtime/registry/instagram-embed.d.ts +2 -17
  90. package/dist/runtime/registry/instagram-embed.js +4 -19
  91. package/dist/runtime/registry/intercom.d.ts +3 -11
  92. package/dist/runtime/registry/intercom.js +2 -12
  93. package/dist/runtime/registry/matomo-analytics.d.ts +2 -11
  94. package/dist/runtime/registry/matomo-analytics.js +3 -12
  95. package/dist/runtime/registry/meta-pixel.d.ts +3 -5
  96. package/dist/runtime/registry/meta-pixel.js +2 -4
  97. package/dist/runtime/registry/npm.d.ts +2 -6
  98. package/dist/runtime/registry/npm.js +2 -9
  99. package/dist/runtime/registry/paypal.d.ts +4 -25
  100. package/dist/runtime/registry/paypal.js +3 -66
  101. package/dist/runtime/registry/plausible-analytics.js +18 -13
  102. package/dist/runtime/registry/posthog.d.ts +10 -11
  103. package/dist/runtime/registry/posthog.js +7 -20
  104. package/dist/runtime/registry/reddit-pixel.d.ts +4 -5
  105. package/dist/runtime/registry/reddit-pixel.js +2 -4
  106. package/dist/runtime/registry/rybbit-analytics.d.ts +2 -14
  107. package/dist/runtime/registry/rybbit-analytics.js +7 -19
  108. package/dist/runtime/registry/schemas.d.ts +946 -0
  109. package/dist/runtime/registry/schemas.js +901 -0
  110. package/dist/runtime/registry/segment.d.ts +2 -5
  111. package/dist/runtime/registry/segment.js +2 -5
  112. package/dist/runtime/registry/snapchat-pixel.d.ts +3 -32
  113. package/dist/runtime/registry/snapchat-pixel.js +2 -20
  114. package/dist/runtime/registry/stripe.d.ts +3 -4
  115. package/dist/runtime/registry/stripe.js +2 -4
  116. package/dist/runtime/registry/tiktok-pixel.d.ts +3 -6
  117. package/dist/runtime/registry/tiktok-pixel.js +2 -6
  118. package/dist/runtime/registry/umami-analytics.d.ts +2 -31
  119. package/dist/runtime/registry/umami-analytics.js +2 -36
  120. package/dist/runtime/registry/vercel-analytics.d.ts +29 -0
  121. package/dist/runtime/registry/vercel-analytics.js +84 -0
  122. package/dist/runtime/registry/vimeo-player.d.ts +2 -2
  123. package/dist/runtime/registry/vimeo-player.js +1 -1
  124. package/dist/runtime/registry/x-embed.d.ts +2 -16
  125. package/dist/runtime/registry/x-embed.js +2 -17
  126. package/dist/runtime/registry/x-pixel.d.ts +3 -6
  127. package/dist/runtime/registry/x-pixel.js +2 -5
  128. package/dist/runtime/registry/youtube-player.d.ts +7 -7
  129. package/dist/runtime/registry/youtube-player.js +1 -1
  130. package/dist/runtime/server/google-static-maps-proxy.js +1 -1
  131. package/dist/runtime/server/{sw-handler.d.ts → gravatar-proxy.d.ts} +1 -1
  132. package/dist/runtime/server/gravatar-proxy.js +62 -0
  133. package/dist/runtime/server/instagram-embed-asset.js +2 -1
  134. package/dist/runtime/server/instagram-embed-image.js +2 -1
  135. package/dist/runtime/server/instagram-embed.js +22 -13
  136. package/dist/runtime/server/proxy-handler.js +161 -117
  137. package/dist/runtime/server/utils/privacy.d.ts +45 -1
  138. package/dist/runtime/server/utils/privacy.js +103 -40
  139. package/dist/runtime/server/x-embed.js +3 -2
  140. package/dist/runtime/types.d.ts +35 -24
  141. package/dist/runtime/utils/pure.d.ts +0 -4
  142. package/dist/runtime/utils/pure.js +0 -67
  143. package/dist/runtime/utils.d.ts +3 -3
  144. package/dist/runtime/utils.js +12 -7
  145. package/dist/shared/scripts.Crpn87WB.mjs +318 -0
  146. package/dist/stats.d.mts +39 -0
  147. package/dist/stats.d.ts +39 -0
  148. package/dist/stats.mjs +772 -0
  149. package/dist/types-source.d.mts +19 -0
  150. package/dist/types-source.d.ts +19 -0
  151. package/dist/types-source.mjs +975 -0
  152. package/package.json +42 -31
  153. package/dist/client/_nuxt/B8XOar-X.js +0 -162
  154. package/dist/client/_nuxt/builds/meta/133a46c5-a5c1-4a63-87d1-037947a5bcdb.json +0 -1
  155. package/dist/client/_nuxt/entry.D45OuV0w.css +0 -1
  156. package/dist/client/_nuxt/error-404.B57D-jUQ.css +0 -1
  157. package/dist/client/_nuxt/error-500.DTHUW7BI.css +0 -1
  158. package/dist/runtime/components/ScriptPayPalMarks.d.vue.ts +0 -52
  159. package/dist/runtime/components/ScriptPayPalMarks.vue +0 -69
  160. package/dist/runtime/components/ScriptPayPalMarks.vue.d.ts +0 -52
  161. package/dist/runtime/plugins/sw-register.client.d.ts +0 -2
  162. package/dist/runtime/plugins/sw-register.client.js +0 -12
  163. package/dist/runtime/server/sw-handler.js +0 -25
  164. package/dist/runtime/sw/proxy-sw.template.d.ts +0 -1
  165. package/dist/runtime/sw/proxy-sw.template.js +0 -54
@@ -1,5 +1,16 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
2
  import { $fetch } from "ofetch";
3
+ const LINK_RE = /<link[^>]+rel=["']stylesheet["'][^>]+href=["']([^"']+)["'][^>]*>/gi;
4
+ const LINK_RE_2 = /<link[^>]+href=["']([^"']+)["'][^>]+rel=["']stylesheet["'][^>]*>/gi;
5
+ const RSRC_RE = /url\(\/rsrc\.php([^)]+)\)/g;
6
+ const SCRIPT_RE = /<script[\s\S]*?<\/script>/gi;
7
+ const STYLESHEET_RE = /<link[^>]+rel=["']stylesheet["'][^>]*>/gi;
8
+ const CSS_RE = /<link[^>]+href=["'][^"']+\.css[^"']*["'][^>]*>/gi;
9
+ const NOSCRIPT_RE = /<noscript>[\s\S]*?<\/noscript>/gi;
10
+ const SCONTENT_RE = /https:\/\/scontent[^"'\s),]+\.cdninstagram\.com[^"'\s),]+/g;
11
+ const STATIC_CDN_RE = /https:\/\/static\.cdninstagram\.com[^"'\s),]+/g;
12
+ const LOOKASIDE_RE = /https:\/\/lookaside\.instagram\.com[^"'\s),]+/g;
13
+ const AMP_RE = /&amp;/g;
3
14
  export default defineEventHandler(async (event) => {
4
15
  const query = getQuery(event);
5
16
  const postUrl = query.url;
@@ -27,7 +38,7 @@ export default defineEventHandler(async (event) => {
27
38
  }
28
39
  const pathname = parsedUrl.pathname.endsWith("/") ? parsedUrl.pathname : `${parsedUrl.pathname}/`;
29
40
  const cleanUrl = parsedUrl.origin + pathname;
30
- const embedUrl = cleanUrl + "embed/" + (captions ? "captioned/" : "");
41
+ const embedUrl = `${cleanUrl}embed/${captions ? "captioned/" : ""}`;
31
42
  const html = await $fetch(embedUrl, {
32
43
  headers: {
33
44
  "Accept": "text/html",
@@ -41,14 +52,12 @@ export default defineEventHandler(async (event) => {
41
52
  });
42
53
  });
43
54
  const cssUrls = [];
44
- const linkRegex = /<link[^>]+rel=["']stylesheet["'][^>]+href=["']([^"']+)["'][^>]*>/gi;
45
55
  let match;
46
- while ((match = linkRegex.exec(html)) !== null) {
56
+ while ((match = LINK_RE.exec(html)) !== null) {
47
57
  if (match[1])
48
58
  cssUrls.push(match[1]);
49
59
  }
50
- const linkRegex2 = /<link[^>]+href=["']([^"']+)["'][^>]+rel=["']stylesheet["'][^>]*>/gi;
51
- while ((match = linkRegex2.exec(html)) !== null) {
60
+ while ((match = LINK_RE_2.exec(html)) !== null) {
52
61
  if (match[1])
53
62
  cssUrls.push(match[1]);
54
63
  }
@@ -61,7 +70,7 @@ export default defineEventHandler(async (event) => {
61
70
  );
62
71
  let combinedCss = cssContents.join("\n");
63
72
  combinedCss = combinedCss.replace(
64
- /url\(\/rsrc\.php([^)]+)\)/g,
73
+ RSRC_RE,
65
74
  (_m, path) => `url(/api/_scripts/instagram-embed-asset?url=${encodeURIComponent(`https://static.cdninstagram.com/rsrc.php${path}`)})`
66
75
  );
67
76
  const baseStyles = `
@@ -70,15 +79,15 @@ export default defineEventHandler(async (event) => {
70
79
  .Embed { opacity: 1 !important; visibility: visible !important; }
71
80
  .EmbeddedMedia, .EmbeddedMediaImage { display: block !important; visibility: visible !important; }
72
81
  `;
73
- let rewrittenHtml = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<link[^>]+rel=["']stylesheet["'][^>]*>/gi, "").replace(/<link[^>]+href=["'][^"']+\.css[^"']*["'][^>]*>/gi, "").replace(/<noscript>[\s\S]*?<\/noscript>/gi, "").replace(
74
- /https:\/\/scontent[^"'\s),]+\.cdninstagram\.com[^"'\s),]+/g,
75
- (m) => `/api/_scripts/instagram-embed-image?url=${encodeURIComponent(m.replace(/&amp;/g, "&"))}`
82
+ let rewrittenHtml = html.replace(SCRIPT_RE, "").replace(STYLESHEET_RE, "").replace(CSS_RE, "").replace(NOSCRIPT_RE, "").replace(
83
+ SCONTENT_RE,
84
+ (m) => `/api/_scripts/instagram-embed-image?url=${encodeURIComponent(m.replace(AMP_RE, "&"))}`
76
85
  ).replace(
77
- /https:\/\/static\.cdninstagram\.com[^"'\s),]+/g,
78
- (m) => `/api/_scripts/instagram-embed-asset?url=${encodeURIComponent(m.replace(/&amp;/g, "&"))}`
86
+ STATIC_CDN_RE,
87
+ (m) => `/api/_scripts/instagram-embed-asset?url=${encodeURIComponent(m.replace(AMP_RE, "&"))}`
79
88
  ).replace(
80
- /https:\/\/lookaside\.instagram\.com[^"'\s),]+/g,
81
- (m) => `/api/_scripts/instagram-embed-image?url=${encodeURIComponent(m.replace(/&amp;/g, "&"))}`
89
+ LOOKASIDE_RE,
90
+ (m) => `/api/_scripts/instagram-embed-image?url=${encodeURIComponent(m.replace(AMP_RE, "&"))}`
82
91
  );
83
92
  rewrittenHtml = rewrittenHtml.replace(
84
93
  "</head>",
@@ -1,30 +1,40 @@
1
- import { defineEventHandler, getHeaders, getRequestIP, readBody, getQuery, setResponseHeader, createError } from "h3";
2
1
  import { useRuntimeConfig } from "#imports";
3
- import { useStorage, useNitroApp } from "nitropack/runtime";
4
- import { hash } from "ohash";
5
- import { rewriteScriptUrls } from "../utils/pure.js";
2
+ import { createError, defineEventHandler, getHeaders, getQuery, getRequestIP, getRequestWebStream, readBody, setResponseHeader } from "h3";
3
+ import { useNitroApp } from "nitropack/runtime";
6
4
  import {
7
- FINGERPRINT_HEADERS,
8
- IP_HEADERS,
9
- SENSITIVE_HEADERS,
10
5
  anonymizeIP,
6
+ mergePrivacy,
11
7
  normalizeLanguage,
12
8
  normalizeUserAgent,
9
+ resolvePrivacy,
10
+ SENSITIVE_HEADERS,
13
11
  stripPayloadFingerprinting
14
12
  } from "./utils/privacy.js";
15
- function stripQueryFingerprinting(query) {
16
- const stripped = stripPayloadFingerprinting(query);
13
+ const COMPRESSION_RE = /gzip|deflate|br|compress|base64/i;
14
+ const ROUTE_WILDCARD_RE = /\/\*\*$/;
15
+ const CLIENT_HINT_VERSION_RE = /;v="(\d+)\.[^"]*"/g;
16
+ const SKIP_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["set-cookie", "transfer-encoding", "content-encoding", "content-length"]);
17
+ let sortedRoutesCache;
18
+ function getSortedRoutes(routes) {
19
+ const key = JSON.stringify(routes);
20
+ if (sortedRoutesCache?.key === key)
21
+ return sortedRoutesCache.sorted;
22
+ const sorted = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
23
+ sortedRoutesCache = { key, sorted };
24
+ return sorted;
25
+ }
26
+ function stripQueryFingerprinting(query, privacy) {
27
+ const stripped = stripPayloadFingerprinting(query, privacy);
17
28
  const params = new URLSearchParams();
18
29
  for (const [key, value] of Object.entries(stripped)) {
19
30
  if (value !== void 0 && value !== null) {
20
- params.set(key, String(value));
31
+ params.set(key, typeof value === "object" ? JSON.stringify(value) : String(value));
21
32
  }
22
33
  }
23
- return params.toString();
34
+ return { queryString: params.toString(), stripped };
24
35
  }
25
36
  export default defineEventHandler(async (event) => {
26
37
  const config = useRuntimeConfig();
27
- const nitro = useNitroApp();
28
38
  const proxyConfig = config["nuxt-scripts-proxy"];
29
39
  if (!proxyConfig) {
30
40
  throw createError({
@@ -32,7 +42,7 @@ export default defineEventHandler(async (event) => {
32
42
  statusMessage: "First-party proxy not configured"
33
43
  });
34
44
  }
35
- const { routes, privacy, cacheTtl = 3600, debug = import.meta.dev } = proxyConfig;
45
+ const { routes, privacy: globalPrivacy, routePrivacy, debug = import.meta.dev } = proxyConfig;
36
46
  const path = event.path;
37
47
  const log = debug ? (message, ...args) => {
38
48
  console.debug(message, ...args);
@@ -40,17 +50,18 @@ export default defineEventHandler(async (event) => {
40
50
  };
41
51
  let targetBase;
42
52
  let matchedPrefix;
43
- const sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
44
- for (const [routePattern, target] of sortedRoutes) {
45
- const prefix = routePattern.replace(/\/\*\*$/, "");
53
+ let matchedRoutePattern;
54
+ for (const [routePattern, target] of getSortedRoutes(routes)) {
55
+ const prefix = routePattern.replace(ROUTE_WILDCARD_RE, "");
46
56
  if (path.startsWith(prefix)) {
47
- targetBase = target.replace(/\/\*\*$/, "");
57
+ targetBase = target.replace(ROUTE_WILDCARD_RE, "");
48
58
  matchedPrefix = prefix;
59
+ matchedRoutePattern = routePattern;
49
60
  log("[proxy] Matched:", prefix, "->", targetBase);
50
61
  break;
51
62
  }
52
63
  }
53
- if (!targetBase || !matchedPrefix) {
64
+ if (!targetBase || !matchedPrefix || !matchedRoutePattern) {
54
65
  log("[proxy] No match for path:", path);
55
66
  throw createError({
56
67
  statusCode: 404,
@@ -58,149 +69,197 @@ export default defineEventHandler(async (event) => {
58
69
  message: `No proxy target found for path: ${path}`
59
70
  });
60
71
  }
72
+ const perScriptInput = routePrivacy[matchedRoutePattern];
73
+ if (debug && perScriptInput === void 0) {
74
+ log("[proxy] WARNING: No privacy config for route", matchedRoutePattern, "\u2014 defaulting to full anonymization");
75
+ }
76
+ const perScriptResolved = resolvePrivacy(perScriptInput ?? true);
77
+ const privacy = globalPrivacy !== void 0 ? mergePrivacy(perScriptResolved, globalPrivacy) : perScriptResolved;
78
+ const anyPrivacy = privacy.ip || privacy.userAgent || privacy.language || privacy.screen || privacy.timezone || privacy.hardware;
79
+ const originalHeaders = getHeaders(event);
80
+ const originalQuery = getQuery(event);
81
+ const contentType = originalHeaders["content-type"] || "";
82
+ const compressionParam = originalQuery.compression || "";
83
+ const isBinaryBody = Boolean(
84
+ originalHeaders["content-encoding"] || contentType.includes("octet-stream") || compressionParam && COMPRESSION_RE.test(compressionParam)
85
+ );
61
86
  let targetPath = path.slice(matchedPrefix.length);
62
87
  if (targetPath && !targetPath.startsWith("/")) {
63
- targetPath = "/" + targetPath;
88
+ targetPath = `/${targetPath}`;
64
89
  }
65
90
  let targetUrl = targetBase + targetPath;
66
- if (privacy === "anonymize") {
67
- const query = getQuery(event);
68
- if (Object.keys(query).length > 0) {
69
- const strippedQuery = stripQueryFingerprinting(query);
91
+ let strippedQueryRecord;
92
+ if (anyPrivacy) {
93
+ if (Object.keys(originalQuery).length > 0) {
94
+ const { queryString, stripped } = stripQueryFingerprinting(originalQuery, privacy);
95
+ strippedQueryRecord = stripped;
70
96
  const basePath = targetUrl.split("?")[0] || targetUrl;
71
- targetUrl = strippedQuery ? `${basePath}?${strippedQuery}` : basePath;
97
+ targetUrl = queryString ? `${basePath}?${queryString}` : basePath;
72
98
  }
73
99
  }
74
- const originalHeaders = getHeaders(event);
75
100
  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;
81
- }
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))
101
+ for (const [key, value] of Object.entries(originalHeaders)) {
102
+ if (!value)
103
+ continue;
104
+ const lowerKey = key.toLowerCase();
105
+ if (lowerKey === "host")
106
+ continue;
107
+ if (SENSITIVE_HEADERS.includes(lowerKey))
108
+ continue;
109
+ if (lowerKey === "content-length") {
110
+ if (anyPrivacy && !isBinaryBody)
88
111
  continue;
89
- if (SENSITIVE_HEADERS.includes(lowerKey))
112
+ headers[lowerKey] = value;
113
+ continue;
114
+ }
115
+ 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") {
116
+ if (privacy.ip)
90
117
  continue;
91
- if (lowerKey === "content-length")
118
+ headers[lowerKey] = value;
119
+ continue;
120
+ }
121
+ if (lowerKey === "user-agent") {
122
+ headers[key] = privacy.userAgent ? normalizeUserAgent(value) : value;
123
+ continue;
124
+ }
125
+ if (lowerKey === "accept-language") {
126
+ headers[key] = privacy.language ? normalizeLanguage(value) : value;
127
+ continue;
128
+ }
129
+ if (lowerKey === "sec-ch-ua" || lowerKey === "sec-ch-ua-full-version-list") {
130
+ headers[lowerKey] = privacy.hardware ? value.replace(CLIENT_HINT_VERSION_RE, ';v="$1"') : value;
131
+ continue;
132
+ }
133
+ if (lowerKey === "sec-ch-ua-platform-version" || lowerKey === "sec-ch-ua-arch" || lowerKey === "sec-ch-ua-model" || lowerKey === "sec-ch-ua-bitness") {
134
+ if (privacy.hardware)
92
135
  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
- }
136
+ headers[lowerKey] = value;
137
+ continue;
104
138
  }
139
+ headers[key] = value;
140
+ }
141
+ if (!headers["x-forwarded-for"]) {
105
142
  const clientIP = getRequestIP(event, { xForwardedFor: true });
106
143
  if (clientIP) {
107
- headers["x-forwarded-for"] = anonymizeIP(clientIP);
144
+ if (privacy.ip) {
145
+ headers["x-forwarded-for"] = anonymizeIP(clientIP);
146
+ } else {
147
+ headers["x-forwarded-for"] = clientIP;
148
+ }
108
149
  }
150
+ } else if (privacy.ip) {
151
+ headers["x-forwarded-for"] = headers["x-forwarded-for"].split(",").map((ip) => anonymizeIP(ip.trim())).join(", ");
109
152
  }
110
153
  let body;
111
154
  let rawBody;
112
- const contentType = originalHeaders["content-type"] || "";
155
+ let passthroughBody = false;
113
156
  const method = event.method?.toUpperCase();
114
- const originalQuery = getQuery(event);
115
- if (method === "POST" || method === "PUT" || method === "PATCH") {
116
- rawBody = await readBody(event);
117
- if (privacy === "anonymize" && rawBody) {
118
- if (typeof rawBody === "object") {
119
- body = stripPayloadFingerprinting(rawBody);
120
- } else if (typeof rawBody === "string") {
121
- if (rawBody.startsWith("{") || rawBody.startsWith("[")) {
122
- let parsed = null;
123
- try {
124
- parsed = JSON.parse(rawBody);
125
- } catch {
126
- }
127
- if (parsed && typeof parsed === "object") {
128
- body = stripPayloadFingerprinting(parsed);
157
+ const isWriteMethod = method === "POST" || method === "PUT" || method === "PATCH";
158
+ if (isWriteMethod) {
159
+ if (isBinaryBody || !anyPrivacy) {
160
+ passthroughBody = true;
161
+ } else {
162
+ rawBody = await readBody(event);
163
+ if (rawBody != null) {
164
+ if (Array.isArray(rawBody)) {
165
+ body = rawBody.map(
166
+ (item) => item && typeof item === "object" && !Array.isArray(item) ? stripPayloadFingerprinting(item, privacy) : item
167
+ );
168
+ } else if (typeof rawBody === "object") {
169
+ body = stripPayloadFingerprinting(rawBody, privacy);
170
+ } else if (typeof rawBody === "string") {
171
+ if (rawBody.startsWith("{") || rawBody.startsWith("[")) {
172
+ let parsed = null;
173
+ try {
174
+ parsed = JSON.parse(rawBody);
175
+ } catch {
176
+ }
177
+ if (Array.isArray(parsed)) {
178
+ body = parsed.map(
179
+ (item) => item && typeof item === "object" && !Array.isArray(item) ? stripPayloadFingerprinting(item, privacy) : item
180
+ );
181
+ } else if (parsed && typeof parsed === "object") {
182
+ body = stripPayloadFingerprinting(parsed, privacy);
183
+ } else {
184
+ body = rawBody;
185
+ }
186
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
187
+ const params = new URLSearchParams(rawBody);
188
+ const obj = {};
189
+ params.forEach((value, key) => {
190
+ obj[key] = value;
191
+ });
192
+ const stripped = stripPayloadFingerprinting(obj, privacy);
193
+ const stringified = {};
194
+ for (const [k, v] of Object.entries(stripped)) {
195
+ if (v === void 0 || v === null)
196
+ continue;
197
+ stringified[k] = typeof v === "string" ? v : JSON.stringify(v);
198
+ }
199
+ body = new URLSearchParams(stringified).toString();
129
200
  } else {
130
201
  body = rawBody;
131
202
  }
132
- } else if (contentType.includes("application/x-www-form-urlencoded")) {
133
- const params = new URLSearchParams(rawBody);
134
- const obj = {};
135
- params.forEach((value, key) => {
136
- obj[key] = value;
137
- });
138
- const stripped = stripPayloadFingerprinting(obj);
139
- const stringified = {};
140
- for (const [k, v] of Object.entries(stripped)) {
141
- if (v === void 0 || v === null) continue;
142
- stringified[k] = typeof v === "string" ? v : JSON.stringify(v);
143
- }
144
- body = new URLSearchParams(stringified).toString();
145
203
  } else {
146
204
  body = rawBody;
147
205
  }
148
- } else {
149
- body = rawBody;
150
206
  }
151
- } else {
152
- body = rawBody;
153
207
  }
154
208
  }
209
+ const nitro = useNitroApp();
155
210
  await nitro.hooks.callHook("nuxt-scripts:proxy", {
156
211
  timestamp: Date.now(),
157
212
  path: event.path,
158
213
  targetUrl,
159
214
  method: method || "GET",
160
215
  privacy,
216
+ passthroughBody,
161
217
  original: {
162
218
  headers: { ...originalHeaders },
163
219
  query: originalQuery,
164
- body: rawBody ?? null
220
+ body: passthroughBody ? "<passthrough>" : rawBody ?? null
165
221
  },
166
222
  stripped: {
167
223
  headers,
168
- query: privacy === "anonymize" ? stripPayloadFingerprinting(originalQuery) : originalQuery,
169
- body: body ?? null
224
+ query: strippedQueryRecord ?? originalQuery,
225
+ body: passthroughBody ? "<passthrough>" : body ?? null
170
226
  }
171
227
  });
172
228
  log("[proxy] Fetching:", targetUrl);
173
229
  const controller = new AbortController();
174
230
  const timeoutId = setTimeout(() => controller.abort(), 15e3);
231
+ let fetchBody;
232
+ if (passthroughBody) {
233
+ fetchBody = getRequestWebStream(event);
234
+ } else if (body !== void 0) {
235
+ fetchBody = typeof body === "string" ? body : JSON.stringify(body);
236
+ }
175
237
  let response;
176
238
  try {
177
239
  response = await fetch(targetUrl, {
178
240
  method: method || "GET",
179
241
  headers,
180
- body: body ? typeof body === "string" ? body : JSON.stringify(body) : void 0,
242
+ body: fetchBody,
181
243
  credentials: "omit",
182
244
  // Don't send cookies to third parties
183
- signal: controller.signal
245
+ signal: controller.signal,
246
+ // @ts-expect-error Node fetch supports duplex for streaming request bodies
247
+ duplex: passthroughBody ? "half" : void 0
184
248
  });
185
249
  } catch (err) {
186
250
  clearTimeout(timeoutId);
187
- log("[proxy] Fetch error:", err instanceof Error ? err.message : err);
188
- if (path.includes("/collect") || path.includes("/tr") || path.includes("/events")) {
189
- event.node.res.statusCode = 204;
190
- return "";
191
- }
192
- const isTimeout = err instanceof Error && (err.message.includes("aborted") || err.message.includes("timeout"));
251
+ log("[proxy] Upstream error:", err);
193
252
  throw createError({
194
- statusCode: isTimeout ? 504 : 502,
195
- statusMessage: isTimeout ? "Upstream timeout" : "Bad Gateway",
196
- message: "Failed to reach upstream"
253
+ statusCode: 502,
254
+ statusMessage: "Bad Gateway",
255
+ message: `Proxy upstream request failed: ${targetUrl}`
197
256
  });
257
+ } finally {
258
+ clearTimeout(timeoutId);
198
259
  }
199
- clearTimeout(timeoutId);
200
260
  log("[proxy] Response:", response.status, response.statusText);
201
- const skipHeaders = ["set-cookie", "transfer-encoding", "content-encoding", "content-length"];
202
261
  response.headers.forEach((value, key) => {
203
- if (!skipHeaders.includes(key.toLowerCase())) {
262
+ if (!SKIP_RESPONSE_HEADERS.has(key.toLowerCase())) {
204
263
  setResponseHeader(event, key, value);
205
264
  }
206
265
  });
@@ -209,22 +268,7 @@ export default defineEventHandler(async (event) => {
209
268
  const responseContentType = response.headers.get("content-type") || "";
210
269
  const isTextContent = responseContentType.includes("text") || responseContentType.includes("javascript") || responseContentType.includes("json");
211
270
  if (isTextContent) {
212
- let content = await response.text();
213
- if (responseContentType.includes("javascript") && proxyConfig?.rewrites?.length) {
214
- const cacheKey = `nuxt-scripts:proxy:${hash(targetUrl + JSON.stringify(proxyConfig.rewrites))}`;
215
- const storage = useStorage("cache");
216
- const cached = await storage.getItem(cacheKey);
217
- if (cached && typeof cached === "string") {
218
- log("[proxy] Serving rewritten script from cache");
219
- content = cached;
220
- } else {
221
- content = rewriteScriptUrls(content, proxyConfig.rewrites);
222
- await storage.setItem(cacheKey, content, { ttl: cacheTtl });
223
- log("[proxy] Rewrote URLs in JavaScript response and cached");
224
- }
225
- setResponseHeader(event, "cache-control", `public, max-age=${cacheTtl}, stale-while-revalidate=${cacheTtl * 2}`);
226
- }
227
- return content;
271
+ return await response.text();
228
272
  }
229
273
  return Buffer.from(await response.arrayBuffer());
230
274
  });
@@ -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>;