@nuxt/scripts 1.0.0-beta.1 → 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.
Files changed (70) 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/CD5B-xvT.js +1 -0
  5. package/dist/client/_nuxt/D-kOnTuH.js +162 -0
  6. package/dist/client/_nuxt/DdVDSbUA.js +1 -0
  7. package/dist/client/_nuxt/{DTDyDxvR.js → Ds2G8aQM.js} +1 -1
  8. package/dist/client/_nuxt/builds/latest.json +1 -1
  9. package/dist/client/_nuxt/builds/meta/f1474569-6922-450d-bc3f-4fd5f3e1391a.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 +87 -2
  15. package/dist/module.json +1 -1
  16. package/dist/module.mjs +678 -142
  17. package/dist/registry.mjs +11 -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/posthog.d.ts +1 -0
  34. package/dist/runtime/registry/posthog.js +2 -8
  35. package/dist/runtime/registry/tiktok-pixel.d.ts +1 -0
  36. package/dist/runtime/registry/tiktok-pixel.js +1 -0
  37. package/dist/runtime/registry/x-embed.d.ts +77 -0
  38. package/dist/runtime/registry/x-embed.js +41 -0
  39. package/dist/runtime/server/instagram-embed-asset.d.ts +2 -0
  40. package/dist/runtime/server/instagram-embed-asset.js +42 -0
  41. package/dist/runtime/server/instagram-embed-image.d.ts +2 -0
  42. package/dist/runtime/server/instagram-embed-image.js +54 -0
  43. package/dist/runtime/server/instagram-embed.d.ts +2 -0
  44. package/dist/runtime/server/instagram-embed.js +91 -0
  45. package/dist/runtime/server/proxy-handler.d.ts +6 -0
  46. package/dist/runtime/server/proxy-handler.js +246 -0
  47. package/dist/runtime/server/sw-handler.d.ts +2 -0
  48. package/dist/runtime/server/sw-handler.js +25 -0
  49. package/dist/runtime/server/utils/privacy.d.ts +141 -0
  50. package/dist/runtime/server/utils/privacy.js +309 -0
  51. package/dist/runtime/server/x-embed-image.d.ts +2 -0
  52. package/dist/runtime/server/x-embed-image.js +53 -0
  53. package/dist/runtime/server/x-embed.d.ts +49 -0
  54. package/dist/runtime/server/x-embed.js +31 -0
  55. package/dist/runtime/sw/proxy-sw.template.d.ts +1 -0
  56. package/dist/runtime/sw/proxy-sw.template.js +54 -0
  57. package/dist/runtime/types.d.ts +29 -0
  58. package/dist/runtime/utils/pure.d.ts +13 -0
  59. package/dist/runtime/utils/pure.js +67 -0
  60. package/dist/runtime/utils.d.ts +1 -1
  61. package/dist/runtime/utils.js +2 -1
  62. package/dist/types.d.mts +1 -1
  63. package/package.json +27 -26
  64. package/dist/client/_nuxt/Bdf7Qtwg.js +0 -1
  65. package/dist/client/_nuxt/CoyZWCgl.js +0 -162
  66. package/dist/client/_nuxt/Ds1k3yKJ.js +0 -1
  67. package/dist/client/_nuxt/builds/meta/62574f80-71d4-4f9e-8b96-145c85230d99.json +0 -1
  68. package/dist/client/_nuxt/entry.BjfcJo5q.css +0 -1
  69. package/dist/client/_nuxt/error-404.D45Vtjcx.css +0 -1
  70. 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 || {};
@@ -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
  });
@@ -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,246 @@
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
+ SENSITIVE_HEADERS,
8
+ anonymizeIP,
9
+ normalizeLanguage,
10
+ normalizeUserAgent,
11
+ stripPayloadFingerprinting,
12
+ resolvePrivacy,
13
+ mergePrivacy
14
+ } from "./utils/privacy.js";
15
+ function stripQueryFingerprinting(query, privacy) {
16
+ const stripped = stripPayloadFingerprinting(query, privacy);
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, typeof value === "object" ? JSON.stringify(value) : 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: globalPrivacy, routePrivacy, 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
+ let matchedRoutePattern;
44
+ const sortedRoutes = Object.entries(routes).sort((a, b) => b[0].length - a[0].length);
45
+ for (const [routePattern, target] of sortedRoutes) {
46
+ const prefix = routePattern.replace(/\/\*\*$/, "");
47
+ if (path.startsWith(prefix)) {
48
+ targetBase = target.replace(/\/\*\*$/, "");
49
+ matchedPrefix = prefix;
50
+ matchedRoutePattern = routePattern;
51
+ log("[proxy] Matched:", prefix, "->", targetBase);
52
+ break;
53
+ }
54
+ }
55
+ if (!targetBase || !matchedPrefix || !matchedRoutePattern) {
56
+ log("[proxy] No match for path:", path);
57
+ throw createError({
58
+ statusCode: 404,
59
+ statusMessage: "No proxy route matched",
60
+ message: `No proxy target found for path: ${path}`
61
+ });
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;
70
+ let targetPath = path.slice(matchedPrefix.length);
71
+ if (targetPath && !targetPath.startsWith("/")) {
72
+ targetPath = "/" + targetPath;
73
+ }
74
+ let targetUrl = targetBase + targetPath;
75
+ if (anyPrivacy) {
76
+ const query = getQuery(event);
77
+ if (Object.keys(query).length > 0) {
78
+ const strippedQuery = stripQueryFingerprinting(query, privacy);
79
+ const basePath = targetUrl.split("?")[0] || targetUrl;
80
+ targetUrl = strippedQuery ? `${basePath}?${strippedQuery}` : basePath;
81
+ }
82
+ }
83
+ const originalHeaders = getHeaders(event);
84
+ const headers = {};
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;
94
+ }
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;
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"]) {
115
+ const clientIP = getRequestIP(event, { xForwardedFor: true });
116
+ if (clientIP) {
117
+ if (privacy.ip) {
118
+ headers["x-forwarded-for"] = anonymizeIP(clientIP);
119
+ } else {
120
+ headers["x-forwarded-for"] = clientIP;
121
+ }
122
+ }
123
+ } else if (privacy.ip) {
124
+ headers["x-forwarded-for"] = headers["x-forwarded-for"].split(",").map((ip) => anonymizeIP(ip.trim())).join(", ");
125
+ }
126
+ let body;
127
+ let rawBody;
128
+ const contentType = originalHeaders["content-type"] || "";
129
+ const method = event.method?.toUpperCase();
130
+ const originalQuery = getQuery(event);
131
+ if (method === "POST" || method === "PUT" || method === "PATCH") {
132
+ rawBody = await readBody(event);
133
+ if (anyPrivacy && rawBody) {
134
+ if (typeof rawBody === "object") {
135
+ body = stripPayloadFingerprinting(rawBody, privacy);
136
+ } else if (typeof rawBody === "string") {
137
+ if (rawBody.startsWith("{") || rawBody.startsWith("[")) {
138
+ let parsed = null;
139
+ try {
140
+ parsed = JSON.parse(rawBody);
141
+ } catch {
142
+ }
143
+ if (parsed && typeof parsed === "object") {
144
+ body = stripPayloadFingerprinting(parsed, privacy);
145
+ } else {
146
+ body = rawBody;
147
+ }
148
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
149
+ const params = new URLSearchParams(rawBody);
150
+ const obj = {};
151
+ params.forEach((value, key) => {
152
+ obj[key] = value;
153
+ });
154
+ const stripped = stripPayloadFingerprinting(obj, privacy);
155
+ const stringified = {};
156
+ for (const [k, v] of Object.entries(stripped)) {
157
+ if (v === void 0 || v === null) continue;
158
+ stringified[k] = typeof v === "string" ? v : JSON.stringify(v);
159
+ }
160
+ body = new URLSearchParams(stringified).toString();
161
+ } else {
162
+ body = rawBody;
163
+ }
164
+ } else {
165
+ body = rawBody;
166
+ }
167
+ } else {
168
+ body = rawBody;
169
+ }
170
+ }
171
+ await nitro.hooks.callHook("nuxt-scripts:proxy", {
172
+ timestamp: Date.now(),
173
+ path: event.path,
174
+ targetUrl,
175
+ method: method || "GET",
176
+ privacy,
177
+ original: {
178
+ headers: { ...originalHeaders },
179
+ query: originalQuery,
180
+ body: rawBody ?? null
181
+ },
182
+ stripped: {
183
+ headers,
184
+ query: anyPrivacy ? stripPayloadFingerprinting(originalQuery, privacy) : originalQuery,
185
+ body: body ?? null
186
+ }
187
+ });
188
+ log("[proxy] Fetching:", targetUrl);
189
+ const controller = new AbortController();
190
+ const timeoutId = setTimeout(() => controller.abort(), 15e3);
191
+ let response;
192
+ try {
193
+ response = await fetch(targetUrl, {
194
+ method: method || "GET",
195
+ headers,
196
+ body: body ? typeof body === "string" ? body : JSON.stringify(body) : void 0,
197
+ credentials: "omit",
198
+ // Don't send cookies to third parties
199
+ signal: controller.signal
200
+ });
201
+ } catch (err) {
202
+ clearTimeout(timeoutId);
203
+ log("[proxy] Fetch error:", err instanceof Error ? err.message : err);
204
+ if (path.includes("/collect") || path.includes("/tr") || path.includes("/events")) {
205
+ event.node.res.statusCode = 204;
206
+ return "";
207
+ }
208
+ const isTimeout = err instanceof Error && (err.message.includes("aborted") || err.message.includes("timeout"));
209
+ throw createError({
210
+ statusCode: isTimeout ? 504 : 502,
211
+ statusMessage: isTimeout ? "Upstream timeout" : "Bad Gateway",
212
+ message: "Failed to reach upstream"
213
+ });
214
+ }
215
+ clearTimeout(timeoutId);
216
+ log("[proxy] Response:", response.status, response.statusText);
217
+ const skipHeaders = ["set-cookie", "transfer-encoding", "content-encoding", "content-length"];
218
+ response.headers.forEach((value, key) => {
219
+ if (!skipHeaders.includes(key.toLowerCase())) {
220
+ setResponseHeader(event, key, value);
221
+ }
222
+ });
223
+ event.node.res.statusCode = response.status;
224
+ event.node.res.statusMessage = response.statusText;
225
+ const responseContentType = response.headers.get("content-type") || "";
226
+ const isTextContent = responseContentType.includes("text") || responseContentType.includes("javascript") || responseContentType.includes("json");
227
+ if (isTextContent) {
228
+ let content = await response.text();
229
+ if (responseContentType.includes("javascript") && proxyConfig?.rewrites?.length) {
230
+ const cacheKey = `nuxt-scripts:proxy:${hash(targetUrl + JSON.stringify(proxyConfig.rewrites))}`;
231
+ const storage = useStorage("cache");
232
+ const cached = await storage.getItem(cacheKey);
233
+ if (cached && typeof cached === "string") {
234
+ log("[proxy] Serving rewritten script from cache");
235
+ content = cached;
236
+ } else {
237
+ content = rewriteScriptUrls(content, proxyConfig.rewrites);
238
+ await storage.setItem(cacheKey, content, { ttl: cacheTtl });
239
+ log("[proxy] Rewrote URLs in JavaScript response and cached");
240
+ }
241
+ setResponseHeader(event, "cache-control", `public, max-age=${cacheTtl}, stale-while-revalidate=${cacheTtl * 2}`);
242
+ }
243
+ return content;
244
+ }
245
+ return Buffer.from(await response.arrayBuffer());
246
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string>>;
2
+ export default _default;