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

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 (68) hide show
  1. package/README.md +6 -0
  2. package/dist/client/200.html +1 -1
  3. package/dist/client/404.html +1 -1
  4. package/dist/client/_nuxt/B66N9HCo.js +1 -0
  5. package/dist/client/_nuxt/B8XOar-X.js +162 -0
  6. package/dist/client/_nuxt/{DTDyDxvR.js → DfLgoB--.js} +1 -1
  7. package/dist/client/_nuxt/DvH517bE.js +1 -0
  8. package/dist/client/_nuxt/builds/latest.json +1 -1
  9. package/dist/client/_nuxt/builds/meta/133a46c5-a5c1-4a63-87d1-037947a5bcdb.json +1 -0
  10. package/dist/client/_nuxt/entry.D45OuV0w.css +1 -0
  11. package/dist/client/_nuxt/error-404.B57D-jUQ.css +1 -0
  12. package/dist/client/_nuxt/error-500.DTHUW7BI.css +1 -0
  13. package/dist/client/index.html +1 -1
  14. package/dist/module.d.mts +80 -2
  15. package/dist/module.json +1 -1
  16. package/dist/module.mjs +630 -142
  17. package/dist/registry.mjs +10 -0
  18. package/dist/runtime/components/ScriptInstagramEmbed.d.vue.ts +53 -0
  19. package/dist/runtime/components/ScriptInstagramEmbed.vue +38 -0
  20. package/dist/runtime/components/ScriptInstagramEmbed.vue.d.ts +53 -0
  21. package/dist/runtime/components/ScriptXEmbed.d.vue.ts +82 -0
  22. package/dist/runtime/components/ScriptXEmbed.vue +76 -0
  23. package/dist/runtime/components/ScriptXEmbed.vue.d.ts +82 -0
  24. package/dist/runtime/composables/useScript.js +24 -3
  25. package/dist/runtime/composables/useScriptTriggerServiceWorker.d.ts +7 -0
  26. package/dist/runtime/composables/useScriptTriggerServiceWorker.js +39 -0
  27. package/dist/runtime/plugins/sw-register.client.d.ts +2 -0
  28. package/dist/runtime/plugins/sw-register.client.js +12 -0
  29. package/dist/runtime/registry/instagram-embed.d.ts +23 -0
  30. package/dist/runtime/registry/instagram-embed.js +22 -0
  31. package/dist/runtime/registry/lemon-squeezy.d.ts +0 -1
  32. package/dist/runtime/registry/plausible-analytics.js +2 -2
  33. package/dist/runtime/registry/tiktok-pixel.d.ts +1 -0
  34. package/dist/runtime/registry/tiktok-pixel.js +1 -0
  35. package/dist/runtime/registry/x-embed.d.ts +77 -0
  36. package/dist/runtime/registry/x-embed.js +41 -0
  37. package/dist/runtime/server/instagram-embed-asset.d.ts +2 -0
  38. package/dist/runtime/server/instagram-embed-asset.js +42 -0
  39. package/dist/runtime/server/instagram-embed-image.d.ts +2 -0
  40. package/dist/runtime/server/instagram-embed-image.js +54 -0
  41. package/dist/runtime/server/instagram-embed.d.ts +2 -0
  42. package/dist/runtime/server/instagram-embed.js +91 -0
  43. package/dist/runtime/server/proxy-handler.d.ts +6 -0
  44. package/dist/runtime/server/proxy-handler.js +230 -0
  45. package/dist/runtime/server/sw-handler.d.ts +2 -0
  46. package/dist/runtime/server/sw-handler.js +25 -0
  47. package/dist/runtime/server/utils/privacy.d.ts +97 -0
  48. package/dist/runtime/server/utils/privacy.js +268 -0
  49. package/dist/runtime/server/x-embed-image.d.ts +2 -0
  50. package/dist/runtime/server/x-embed-image.js +53 -0
  51. package/dist/runtime/server/x-embed.d.ts +49 -0
  52. package/dist/runtime/server/x-embed.js +31 -0
  53. package/dist/runtime/sw/proxy-sw.template.d.ts +1 -0
  54. package/dist/runtime/sw/proxy-sw.template.js +54 -0
  55. package/dist/runtime/types.d.ts +29 -0
  56. package/dist/runtime/utils/pure.d.ts +13 -0
  57. package/dist/runtime/utils/pure.js +67 -0
  58. package/dist/runtime/utils.d.ts +1 -1
  59. package/dist/runtime/utils.js +2 -1
  60. package/dist/types.d.mts +1 -1
  61. package/package.json +27 -26
  62. package/dist/client/_nuxt/Bdf7Qtwg.js +0 -1
  63. package/dist/client/_nuxt/CoyZWCgl.js +0 -162
  64. package/dist/client/_nuxt/Ds1k3yKJ.js +0 -1
  65. package/dist/client/_nuxt/builds/meta/62574f80-71d4-4f9e-8b96-145c85230d99.json +0 -1
  66. package/dist/client/_nuxt/entry.BjfcJo5q.css +0 -1
  67. package/dist/client/_nuxt/error-404.D45Vtjcx.css +0 -1
  68. package/dist/client/_nuxt/error-500.BOm1rWQf.css +0 -1
@@ -76,8 +76,8 @@ export function useScriptPlausibleAnalytics(_options) {
76
76
  },
77
77
  clientInit: import.meta.server ? void 0 : () => {
78
78
  const w = window;
79
- w.plausible = w.plausible || function() {
80
- (w.plausible.q = w.plausible.q || []).push(arguments);
79
+ w.plausible = w.plausible || function(...args) {
80
+ (w.plausible.q = w.plausible.q || []).push(args);
81
81
  };
82
82
  w.plausible.init = w.plausible.init || function(i) {
83
83
  w.plausible.o = i || {};
@@ -32,6 +32,7 @@ export interface TikTokPixelApi {
32
32
  }
33
33
  declare global {
34
34
  interface Window extends TikTokPixelApi {
35
+ TiktokAnalyticsObject: string;
35
36
  }
36
37
  }
37
38
  export declare const TikTokPixelOptions: import("valibot").ObjectSchema<{
@@ -22,6 +22,7 @@ export function useScriptTikTokPixel(_options) {
22
22
  }
23
23
  },
24
24
  clientInit: import.meta.server ? void 0 : () => {
25
+ window.TiktokAnalyticsObject = "ttq";
25
26
  const ttq = window.ttq = function(...params) {
26
27
  if (ttq.callMethod) {
27
28
  ttq.callMethod(...params);
@@ -0,0 +1,77 @@
1
+ import type { RegistryScriptInput } from '#nuxt-scripts/types';
2
+ export interface XEmbedTweetData {
3
+ id_str: string;
4
+ text: string;
5
+ created_at: string;
6
+ favorite_count: number;
7
+ conversation_count: number;
8
+ user: {
9
+ name: string;
10
+ screen_name: string;
11
+ profile_image_url_https: string;
12
+ verified?: boolean;
13
+ is_blue_verified?: boolean;
14
+ };
15
+ entities?: {
16
+ media?: Array<{
17
+ media_url_https: string;
18
+ type: string;
19
+ sizes: Record<string, {
20
+ w: number;
21
+ h: number;
22
+ }>;
23
+ }>;
24
+ urls?: Array<{
25
+ url: string;
26
+ expanded_url: string;
27
+ display_url: string;
28
+ }>;
29
+ };
30
+ photos?: Array<{
31
+ url: string;
32
+ width: number;
33
+ height: number;
34
+ }>;
35
+ video?: {
36
+ poster: string;
37
+ variants: Array<{
38
+ type: string;
39
+ src: string;
40
+ }>;
41
+ };
42
+ quoted_tweet?: XEmbedTweetData;
43
+ parent?: {
44
+ user: {
45
+ screen_name: string;
46
+ };
47
+ };
48
+ }
49
+ export declare const XEmbedOptions: import("valibot").ObjectSchema<{
50
+ /**
51
+ * The tweet ID to embed
52
+ */
53
+ readonly tweetId: import("valibot").StringSchema<undefined>;
54
+ /**
55
+ * Optional: Custom API endpoint for fetching tweet data
56
+ * @default '/api/_scripts/x-embed'
57
+ */
58
+ readonly apiEndpoint: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
59
+ /**
60
+ * Optional: Custom image proxy endpoint
61
+ * @default '/api/_scripts/x-embed-image'
62
+ */
63
+ readonly imageProxyEndpoint: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
64
+ }, undefined>;
65
+ export type XEmbedInput = RegistryScriptInput<typeof XEmbedOptions, false, false, false>;
66
+ /**
67
+ * Proxy an X/Twitter image URL through the server
68
+ */
69
+ export declare function proxyXImageUrl(url: string, proxyEndpoint?: string): string;
70
+ /**
71
+ * Format a tweet date for display
72
+ */
73
+ export declare function formatTweetDate(dateString: string): string;
74
+ /**
75
+ * Format a number for display (e.g., 1234 -> 1.2K)
76
+ */
77
+ export declare function formatCount(count: number): string;
@@ -0,0 +1,41 @@
1
+ import { object, optional, string } from "#nuxt-scripts-validator";
2
+ export const XEmbedOptions = object({
3
+ /**
4
+ * The tweet ID to embed
5
+ */
6
+ tweetId: string(),
7
+ /**
8
+ * Optional: Custom API endpoint for fetching tweet data
9
+ * @default '/api/_scripts/x-embed'
10
+ */
11
+ apiEndpoint: optional(string()),
12
+ /**
13
+ * Optional: Custom image proxy endpoint
14
+ * @default '/api/_scripts/x-embed-image'
15
+ */
16
+ imageProxyEndpoint: optional(string())
17
+ });
18
+ export function proxyXImageUrl(url, proxyEndpoint = "/api/_scripts/x-embed-image") {
19
+ const separator = proxyEndpoint.includes("?") ? "&" : "?";
20
+ return `${proxyEndpoint}${separator}url=${encodeURIComponent(url)}`;
21
+ }
22
+ export function formatTweetDate(dateString) {
23
+ const date = new Date(dateString);
24
+ const time = date.toLocaleString("en-US", {
25
+ hour: "numeric",
26
+ minute: "numeric",
27
+ hour12: true,
28
+ timeZone: "UTC"
29
+ });
30
+ const day = date.toLocaleString("en-US", { month: "short", timeZone: "UTC" });
31
+ return `${time} \xB7 ${day} ${date.getUTCDate()}, ${date.getUTCFullYear()}`;
32
+ }
33
+ export function formatCount(count) {
34
+ if (count >= 1e6) {
35
+ return `${(count / 1e6).toFixed(1)}M`;
36
+ }
37
+ if (count >= 1e3) {
38
+ return `${(count / 1e3).toFixed(1)}K`;
39
+ }
40
+ return count.toString();
41
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
+ export default _default;
@@ -0,0 +1,42 @@
1
+ import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
+ import { $fetch } from "ofetch";
3
+ export default defineEventHandler(async (event) => {
4
+ const query = getQuery(event);
5
+ const url = query.url?.replace(/&amp;/g, "&");
6
+ if (!url) {
7
+ throw createError({
8
+ statusCode: 400,
9
+ statusMessage: "Asset URL is required"
10
+ });
11
+ }
12
+ let parsedUrl;
13
+ try {
14
+ parsedUrl = new URL(url);
15
+ } catch {
16
+ throw createError({
17
+ statusCode: 400,
18
+ statusMessage: "Invalid asset URL"
19
+ });
20
+ }
21
+ if (parsedUrl.hostname !== "static.cdninstagram.com") {
22
+ throw createError({
23
+ statusCode: 403,
24
+ statusMessage: "Domain not allowed"
25
+ });
26
+ }
27
+ const response = await $fetch.raw(url, {
28
+ headers: {
29
+ "Accept": "*/*",
30
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
31
+ }
32
+ }).catch((error) => {
33
+ throw createError({
34
+ statusCode: error.statusCode || 500,
35
+ statusMessage: error.statusMessage || "Failed to fetch asset"
36
+ });
37
+ });
38
+ const contentType = response.headers.get("content-type") || "application/octet-stream";
39
+ setHeader(event, "Content-Type", contentType);
40
+ setHeader(event, "Cache-Control", "public, max-age=86400, s-maxage=86400");
41
+ return response._data;
42
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
+ export default _default;
@@ -0,0 +1,54 @@
1
+ import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
+ import { $fetch } from "ofetch";
3
+ export default defineEventHandler(async (event) => {
4
+ const query = getQuery(event);
5
+ const url = query.url?.replace(/&amp;/g, "&");
6
+ if (!url) {
7
+ throw createError({
8
+ statusCode: 400,
9
+ statusMessage: "Image URL is required"
10
+ });
11
+ }
12
+ let parsedUrl;
13
+ try {
14
+ parsedUrl = new URL(url);
15
+ } catch {
16
+ throw createError({
17
+ statusCode: 400,
18
+ statusMessage: "Invalid image URL"
19
+ });
20
+ }
21
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
22
+ throw createError({
23
+ statusCode: 400,
24
+ statusMessage: "Invalid URL scheme"
25
+ });
26
+ }
27
+ if (!parsedUrl.hostname.endsWith(".cdninstagram.com") && parsedUrl.hostname !== "scontent.cdninstagram.com") {
28
+ throw createError({
29
+ statusCode: 403,
30
+ statusMessage: "Domain not allowed"
31
+ });
32
+ }
33
+ const response = await $fetch.raw(url, {
34
+ redirect: "manual",
35
+ headers: {
36
+ "Accept": "image/webp,image/jpeg,image/png,image/*,*/*;q=0.8",
37
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
38
+ }
39
+ }).catch((error) => {
40
+ throw createError({
41
+ statusCode: error.statusCode || 500,
42
+ statusMessage: error.statusMessage || "Failed to fetch image"
43
+ });
44
+ });
45
+ if (response.status >= 300 && response.status < 400) {
46
+ throw createError({
47
+ statusCode: 403,
48
+ statusMessage: "Redirects not allowed"
49
+ });
50
+ }
51
+ setHeader(event, "Content-Type", response.headers.get("content-type") || "image/jpeg");
52
+ setHeader(event, "Cache-Control", "public, max-age=3600, s-maxage=3600");
53
+ return response._data;
54
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string>>;
2
+ export default _default;
@@ -0,0 +1,91 @@
1
+ import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
+ import { $fetch } from "ofetch";
3
+ export default defineEventHandler(async (event) => {
4
+ const query = getQuery(event);
5
+ const postUrl = query.url;
6
+ const captions = query.captions === "true";
7
+ if (!postUrl) {
8
+ throw createError({
9
+ statusCode: 400,
10
+ statusMessage: "Post URL is required"
11
+ });
12
+ }
13
+ let parsedUrl;
14
+ try {
15
+ parsedUrl = new URL(postUrl);
16
+ } catch {
17
+ throw createError({
18
+ statusCode: 400,
19
+ statusMessage: "Invalid postUrl"
20
+ });
21
+ }
22
+ if (!["instagram.com", "www.instagram.com"].includes(parsedUrl.hostname)) {
23
+ throw createError({
24
+ statusCode: 400,
25
+ statusMessage: "Invalid Instagram URL"
26
+ });
27
+ }
28
+ const pathname = parsedUrl.pathname.endsWith("/") ? parsedUrl.pathname : `${parsedUrl.pathname}/`;
29
+ const cleanUrl = parsedUrl.origin + pathname;
30
+ const embedUrl = cleanUrl + "embed/" + (captions ? "captioned/" : "");
31
+ const html = await $fetch(embedUrl, {
32
+ headers: {
33
+ "Accept": "text/html",
34
+ // Use simple UA - full Chrome UA triggers JS-heavy version without static content
35
+ "User-Agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
36
+ }
37
+ }).catch((error) => {
38
+ throw createError({
39
+ statusCode: error.statusCode || 500,
40
+ statusMessage: error.statusMessage || "Failed to fetch Instagram embed"
41
+ });
42
+ });
43
+ const cssUrls = [];
44
+ const linkRegex = /<link[^>]+rel=["']stylesheet["'][^>]+href=["']([^"']+)["'][^>]*>/gi;
45
+ let match;
46
+ while ((match = linkRegex.exec(html)) !== null) {
47
+ if (match[1])
48
+ cssUrls.push(match[1]);
49
+ }
50
+ const linkRegex2 = /<link[^>]+href=["']([^"']+)["'][^>]+rel=["']stylesheet["'][^>]*>/gi;
51
+ while ((match = linkRegex2.exec(html)) !== null) {
52
+ if (match[1])
53
+ cssUrls.push(match[1]);
54
+ }
55
+ const cssContents = await Promise.all(
56
+ cssUrls.map(
57
+ (url) => $fetch(url, {
58
+ headers: { Accept: "text/css" }
59
+ }).catch(() => "")
60
+ )
61
+ );
62
+ let combinedCss = cssContents.join("\n");
63
+ combinedCss = combinedCss.replace(
64
+ /url\(\/rsrc\.php([^)]+)\)/g,
65
+ (_m, path) => `url(/api/_scripts/instagram-embed-asset?url=${encodeURIComponent(`https://static.cdninstagram.com/rsrc.php${path}`)})`
66
+ );
67
+ const baseStyles = `
68
+ html { background: white; max-width: 540px; width: calc(100% - 2px); border-radius: 3px; border: 1px solid rgb(219, 219, 219); display: block; margin: 0px 0px 12px; min-width: 326px; padding: 0px; }
69
+ #splash-screen { display: none !important; }
70
+ .Embed { opacity: 1 !important; visibility: visible !important; }
71
+ .EmbeddedMedia, .EmbeddedMediaImage { display: block !important; visibility: visible !important; }
72
+ `;
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, "&"))}`
76
+ ).replace(
77
+ /https:\/\/static\.cdninstagram\.com[^"'\s),]+/g,
78
+ (m) => `/api/_scripts/instagram-embed-asset?url=${encodeURIComponent(m.replace(/&amp;/g, "&"))}`
79
+ ).replace(
80
+ /https:\/\/lookaside\.instagram\.com[^"'\s),]+/g,
81
+ (m) => `/api/_scripts/instagram-embed-image?url=${encodeURIComponent(m.replace(/&amp;/g, "&"))}`
82
+ );
83
+ rewrittenHtml = rewrittenHtml.replace(
84
+ "</head>",
85
+ `<style>${baseStyles}
86
+ ${combinedCss}</style></head>`
87
+ );
88
+ setHeader(event, "Content-Type", "text/html");
89
+ setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
90
+ return rewrittenHtml;
91
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Privacy-aware proxy handler for first-party script collection endpoints.
3
+ * Routes requests to third-party analytics while protecting user privacy.
4
+ */
5
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | Buffer<ArrayBuffer>>>;
6
+ export default _default;
@@ -0,0 +1,230 @@
1
+ import { defineEventHandler, getHeaders, getRequestIP, readBody, getQuery, setResponseHeader, createError } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { useStorage, useNitroApp } from "nitropack/runtime";
4
+ import { hash } from "ohash";
5
+ import { rewriteScriptUrls } from "../utils/pure.js";
6
+ import {
7
+ FINGERPRINT_HEADERS,
8
+ IP_HEADERS,
9
+ SENSITIVE_HEADERS,
10
+ anonymizeIP,
11
+ normalizeLanguage,
12
+ normalizeUserAgent,
13
+ stripPayloadFingerprinting
14
+ } from "./utils/privacy.js";
15
+ function stripQueryFingerprinting(query) {
16
+ const stripped = stripPayloadFingerprinting(query);
17
+ const params = new URLSearchParams();
18
+ for (const [key, value] of Object.entries(stripped)) {
19
+ if (value !== void 0 && value !== null) {
20
+ params.set(key, String(value));
21
+ }
22
+ }
23
+ return params.toString();
24
+ }
25
+ export default defineEventHandler(async (event) => {
26
+ const config = useRuntimeConfig();
27
+ const nitro = useNitroApp();
28
+ const proxyConfig = config["nuxt-scripts-proxy"];
29
+ if (!proxyConfig) {
30
+ throw createError({
31
+ statusCode: 500,
32
+ statusMessage: "First-party proxy not configured"
33
+ });
34
+ }
35
+ const { routes, privacy, cacheTtl = 3600, debug = import.meta.dev } = proxyConfig;
36
+ const path = event.path;
37
+ const log = debug ? (message, ...args) => {
38
+ console.debug(message, ...args);
39
+ } : () => {
40
+ };
41
+ let targetBase;
42
+ 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(/\/\*\*$/, "");
46
+ if (path.startsWith(prefix)) {
47
+ targetBase = target.replace(/\/\*\*$/, "");
48
+ matchedPrefix = prefix;
49
+ log("[proxy] Matched:", prefix, "->", targetBase);
50
+ break;
51
+ }
52
+ }
53
+ if (!targetBase || !matchedPrefix) {
54
+ log("[proxy] No match for path:", path);
55
+ throw createError({
56
+ statusCode: 404,
57
+ statusMessage: "No proxy route matched",
58
+ message: `No proxy target found for path: ${path}`
59
+ });
60
+ }
61
+ let targetPath = path.slice(matchedPrefix.length);
62
+ if (targetPath && !targetPath.startsWith("/")) {
63
+ targetPath = "/" + targetPath;
64
+ }
65
+ 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);
70
+ const basePath = targetUrl.split("?")[0] || targetUrl;
71
+ targetUrl = strippedQuery ? `${basePath}?${strippedQuery}` : basePath;
72
+ }
73
+ }
74
+ const originalHeaders = getHeaders(event);
75
+ 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))
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
+ }
104
+ }
105
+ const clientIP = getRequestIP(event, { xForwardedFor: true });
106
+ if (clientIP) {
107
+ headers["x-forwarded-for"] = anonymizeIP(clientIP);
108
+ }
109
+ }
110
+ let body;
111
+ let rawBody;
112
+ const contentType = originalHeaders["content-type"] || "";
113
+ 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);
129
+ } else {
130
+ body = rawBody;
131
+ }
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
+ } else {
146
+ body = rawBody;
147
+ }
148
+ } else {
149
+ body = rawBody;
150
+ }
151
+ } else {
152
+ body = rawBody;
153
+ }
154
+ }
155
+ await nitro.hooks.callHook("nuxt-scripts:proxy", {
156
+ timestamp: Date.now(),
157
+ path: event.path,
158
+ targetUrl,
159
+ method: method || "GET",
160
+ privacy,
161
+ original: {
162
+ headers: { ...originalHeaders },
163
+ query: originalQuery,
164
+ body: rawBody ?? null
165
+ },
166
+ stripped: {
167
+ headers,
168
+ query: privacy === "anonymize" ? stripPayloadFingerprinting(originalQuery) : originalQuery,
169
+ body: body ?? null
170
+ }
171
+ });
172
+ log("[proxy] Fetching:", targetUrl);
173
+ const controller = new AbortController();
174
+ const timeoutId = setTimeout(() => controller.abort(), 15e3);
175
+ let response;
176
+ try {
177
+ response = await fetch(targetUrl, {
178
+ method: method || "GET",
179
+ headers,
180
+ body: body ? typeof body === "string" ? body : JSON.stringify(body) : void 0,
181
+ credentials: "omit",
182
+ // Don't send cookies to third parties
183
+ signal: controller.signal
184
+ });
185
+ } catch (err) {
186
+ 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"));
193
+ throw createError({
194
+ statusCode: isTimeout ? 504 : 502,
195
+ statusMessage: isTimeout ? "Upstream timeout" : "Bad Gateway",
196
+ message: "Failed to reach upstream"
197
+ });
198
+ }
199
+ clearTimeout(timeoutId);
200
+ log("[proxy] Response:", response.status, response.statusText);
201
+ const skipHeaders = ["set-cookie", "transfer-encoding", "content-encoding", "content-length"];
202
+ response.headers.forEach((value, key) => {
203
+ if (!skipHeaders.includes(key.toLowerCase())) {
204
+ setResponseHeader(event, key, value);
205
+ }
206
+ });
207
+ event.node.res.statusCode = response.status;
208
+ event.node.res.statusMessage = response.statusText;
209
+ const responseContentType = response.headers.get("content-type") || "";
210
+ const isTextContent = responseContentType.includes("text") || responseContentType.includes("javascript") || responseContentType.includes("json");
211
+ 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;
228
+ }
229
+ return Buffer.from(await response.arrayBuffer());
230
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string>>;
2
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import { defineEventHandler, setResponseHeader } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ import { parseURL } from "ufo";
4
+ export default defineEventHandler(async (event) => {
5
+ const config = useRuntimeConfig(event);
6
+ const proxyConfig = config["nuxt-scripts-proxy"];
7
+ const swTemplate = config["nuxt-scripts"]?.swTemplate || "";
8
+ const routes = proxyConfig?.routes || {};
9
+ const rules = Object.entries(routes).map(([localPath, proxy]) => {
10
+ const url = parseURL(proxy.replace(/\*\*$/, ""));
11
+ if (!url.host) return null;
12
+ return {
13
+ pattern: url.host,
14
+ pathPrefix: url.pathname || "",
15
+ target: localPath.replace(/\/\*\*$/, "")
16
+ };
17
+ }).filter(Boolean);
18
+ const swCode = `const INTERCEPT_RULES = ${JSON.stringify(rules)};
19
+
20
+ ${swTemplate}`;
21
+ setResponseHeader(event, "Content-Type", "application/javascript");
22
+ setResponseHeader(event, "Service-Worker-Allowed", "/");
23
+ setResponseHeader(event, "Cache-Control", "no-cache");
24
+ return swCode;
25
+ });