@nuxt/scripts 0.13.2 → 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 (98) hide show
  1. package/README.md +15 -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/{Bje-0OHL.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 +106 -4
  15. package/dist/module.json +1 -1
  16. package/dist/module.mjs +705 -173
  17. package/dist/registry.mjs +63 -0
  18. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.d.vue.ts +29 -1
  19. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +35 -10
  20. package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue.d.ts +29 -1
  21. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.d.vue.ts +20 -8
  22. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.vue +2 -2
  23. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.vue.d.ts +20 -8
  24. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsPinElement.vue +7 -1
  25. package/dist/runtime/components/ScriptCrisp.d.vue.ts +1 -1
  26. package/dist/runtime/components/ScriptCrisp.vue.d.ts +1 -1
  27. package/dist/runtime/components/ScriptInstagramEmbed.d.vue.ts +53 -0
  28. package/dist/runtime/components/ScriptInstagramEmbed.vue +38 -0
  29. package/dist/runtime/components/ScriptInstagramEmbed.vue.d.ts +53 -0
  30. package/dist/runtime/components/ScriptIntercom.d.vue.ts +1 -1
  31. package/dist/runtime/components/ScriptIntercom.vue.d.ts +1 -1
  32. package/dist/runtime/components/ScriptVimeoPlayer.d.vue.ts +2 -2
  33. package/dist/runtime/components/ScriptVimeoPlayer.vue.d.ts +2 -2
  34. package/dist/runtime/components/ScriptXEmbed.d.vue.ts +82 -0
  35. package/dist/runtime/components/ScriptXEmbed.vue +76 -0
  36. package/dist/runtime/components/ScriptXEmbed.vue.d.ts +82 -0
  37. package/dist/runtime/components/ScriptYouTubePlayer.d.vue.ts +12 -1
  38. package/dist/runtime/components/ScriptYouTubePlayer.vue +41 -16
  39. package/dist/runtime/components/ScriptYouTubePlayer.vue.d.ts +12 -1
  40. package/dist/runtime/composables/useScript.js +34 -3
  41. package/dist/runtime/composables/useScriptTriggerServiceWorker.d.ts +7 -0
  42. package/dist/runtime/composables/useScriptTriggerServiceWorker.js +39 -0
  43. package/dist/runtime/npm-script-stub.d.ts +20 -0
  44. package/dist/runtime/npm-script-stub.js +73 -0
  45. package/dist/runtime/plugins/sw-register.client.d.ts +2 -0
  46. package/dist/runtime/plugins/sw-register.client.js +12 -0
  47. package/dist/runtime/registry/google-recaptcha.d.ts +27 -0
  48. package/dist/runtime/registry/google-recaptcha.js +45 -0
  49. package/dist/runtime/registry/google-sign-in.d.ts +84 -0
  50. package/dist/runtime/registry/google-sign-in.js +50 -0
  51. package/dist/runtime/registry/google-tag-manager.d.ts +3 -1
  52. package/dist/runtime/registry/google-tag-manager.js +15 -5
  53. package/dist/runtime/registry/instagram-embed.d.ts +23 -0
  54. package/dist/runtime/registry/instagram-embed.js +22 -0
  55. package/dist/runtime/registry/lemon-squeezy.d.ts +0 -1
  56. package/dist/runtime/registry/matomo-analytics.js +1 -1
  57. package/dist/runtime/registry/plausible-analytics.js +8 -6
  58. package/dist/runtime/registry/posthog.d.ts +26 -0
  59. package/dist/runtime/registry/posthog.js +92 -0
  60. package/dist/runtime/registry/rybbit-analytics.js +38 -8
  61. package/dist/runtime/registry/tiktok-pixel.d.ts +44 -0
  62. package/dist/runtime/registry/tiktok-pixel.js +44 -0
  63. package/dist/runtime/registry/x-embed.d.ts +77 -0
  64. package/dist/runtime/registry/x-embed.js +41 -0
  65. package/dist/runtime/server/google-static-maps-proxy.d.ts +2 -0
  66. package/dist/runtime/server/google-static-maps-proxy.js +54 -0
  67. package/dist/runtime/server/instagram-embed-asset.d.ts +2 -0
  68. package/dist/runtime/server/instagram-embed-asset.js +42 -0
  69. package/dist/runtime/server/instagram-embed-image.d.ts +2 -0
  70. package/dist/runtime/server/instagram-embed-image.js +54 -0
  71. package/dist/runtime/server/instagram-embed.d.ts +2 -0
  72. package/dist/runtime/server/instagram-embed.js +91 -0
  73. package/dist/runtime/server/proxy-handler.d.ts +6 -0
  74. package/dist/runtime/server/proxy-handler.js +230 -0
  75. package/dist/runtime/server/sw-handler.d.ts +2 -0
  76. package/dist/runtime/server/sw-handler.js +25 -0
  77. package/dist/runtime/server/utils/privacy.d.ts +97 -0
  78. package/dist/runtime/server/utils/privacy.js +268 -0
  79. package/dist/runtime/server/x-embed-image.d.ts +2 -0
  80. package/dist/runtime/server/x-embed-image.js +53 -0
  81. package/dist/runtime/server/x-embed.d.ts +49 -0
  82. package/dist/runtime/server/x-embed.js +31 -0
  83. package/dist/runtime/sw/proxy-sw.template.d.ts +1 -0
  84. package/dist/runtime/sw/proxy-sw.template.js +54 -0
  85. package/dist/runtime/types.d.ts +42 -1
  86. package/dist/runtime/utils/pure.d.ts +13 -0
  87. package/dist/runtime/utils/pure.js +67 -0
  88. package/dist/runtime/utils.d.ts +3 -2
  89. package/dist/runtime/utils.js +11 -1
  90. package/dist/types.d.mts +1 -1
  91. package/package.json +39 -32
  92. package/dist/client/_nuxt/DMut0W-e.js +0 -162
  93. package/dist/client/_nuxt/builds/meta/5e0206fe-a683-423c-8d59-2596d0b16fee.json +0 -1
  94. package/dist/client/_nuxt/entry.BjfcJo5q.css +0 -1
  95. package/dist/client/_nuxt/error-404.B0ZhSNwd.css +0 -1
  96. package/dist/client/_nuxt/error-500.D4MdgPaC.css +0 -1
  97. package/dist/client/_nuxt/iNmKC7TZ.js +0 -1
  98. package/dist/client/_nuxt/rttsH3SL.js +0 -1
@@ -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
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Headers that reveal user IP address - stripped in proxy mode,
3
+ * anonymized in anonymize mode.
4
+ */
5
+ export declare const IP_HEADERS: string[];
6
+ /**
7
+ * Headers that enable fingerprinting - normalized in anonymize mode.
8
+ */
9
+ export declare const FINGERPRINT_HEADERS: string[];
10
+ /**
11
+ * Sensitive headers that should never be forwarded to third parties.
12
+ */
13
+ export declare const SENSITIVE_HEADERS: string[];
14
+ /**
15
+ * Payload parameters relevant to privacy.
16
+ *
17
+ * Note: userId and userData are intentionally NOT modified by stripPayloadFingerprinting.
18
+ * Analytics services require user identifiers (cid, uid, fbp, etc.) and user data (ud, email)
19
+ * to function correctly. These are listed here for documentation and param-detection tests only.
20
+ * The privacy model anonymizes device/browser fingerprinting while preserving user-level analytics IDs.
21
+ */
22
+ export declare const STRIP_PARAMS: {
23
+ ip: string[];
24
+ userId: string[];
25
+ userData: string[];
26
+ screen: string[];
27
+ hardware: string[];
28
+ platform: string[];
29
+ version: string[];
30
+ browserVersion: string[];
31
+ browserData: string[];
32
+ location: string[];
33
+ canvas: string[];
34
+ deviceInfo: string[];
35
+ };
36
+ /**
37
+ * Parameters that should be normalized (not stripped).
38
+ */
39
+ export declare const NORMALIZE_PARAMS: {
40
+ language: string[];
41
+ userAgent: string[];
42
+ };
43
+ /**
44
+ * Anonymize an IP address by zeroing trailing segments.
45
+ */
46
+ export declare function anonymizeIP(ip: string): string;
47
+ /**
48
+ * Normalize User-Agent to browser family and major version only.
49
+ */
50
+ export declare function normalizeUserAgent(ua: string): string;
51
+ /**
52
+ * Normalize Accept-Language to primary language tag (preserving country).
53
+ * "en-US,en;q=0.9,fr;q=0.8" → "en-US"
54
+ */
55
+ export declare function normalizeLanguage(lang: string): string;
56
+ /**
57
+ * Generalize screen resolution to 3 coarse device-class buckets (mobile / tablet / desktop).
58
+ * Handles both combined "WxH" strings and individual dimension values (sh, sw).
59
+ *
60
+ * When `dimension` is specified, uses the correct bucket for that axis:
61
+ * - 'width': [1920, 768, 360]
62
+ * - 'height': [1080, 1024, 640]
63
+ * Without `dimension`, individual values use width thresholds (backward-compatible).
64
+ */
65
+ export declare function generalizeScreen(value: unknown, dimension?: 'width' | 'height'): string | number;
66
+ /**
67
+ * Generalize hardware concurrency / device memory to common bucket.
68
+ */
69
+ export declare function generalizeHardware(value: unknown): number;
70
+ /**
71
+ * Generalize a version string to major version, preserving the original format.
72
+ * "6.17.0" → "6.0.0", "143.0.7499.4" → "143.0.0.0"
73
+ */
74
+ export declare function generalizeVersion(value: unknown): string;
75
+ /**
76
+ * Generalize browser version list to major versions, preserving segment count.
77
+ * Handles Snapchat d_bvs format: [,{"brand":"Chrome","version":"143.0.7499.4"}...]
78
+ * Handles GA uafvl format: HeadlessChrome;143.0.7499.4|Chromium;143.0.7499.4|...
79
+ */
80
+ export declare function generalizeBrowserVersions(value: unknown): string;
81
+ /**
82
+ * Generalize timezone to reduce precision.
83
+ * IANA names → UTC offset string, numeric offsets → bucketed to 3-hour intervals.
84
+ */
85
+ export declare function generalizeTimezone(value: unknown): string | number;
86
+ /**
87
+ * Anonymize a combined device-info string (e.g. X/Twitter `dv` param).
88
+ * Parses the delimited string and generalizes fingerprinting components
89
+ * (timezone, language, screen dimensions) while keeping low-entropy values.
90
+ */
91
+ export declare function anonymizeDeviceInfo(value: string): string;
92
+ /**
93
+ * Recursively anonymize fingerprinting data in payload.
94
+ * Fields are generalized or normalized rather than stripped, so endpoints
95
+ * still receive valid data with reduced fingerprinting precision.
96
+ */
97
+ export declare function stripPayloadFingerprinting(payload: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,268 @@
1
+ export const IP_HEADERS = [
2
+ "x-forwarded-for",
3
+ "x-real-ip",
4
+ "forwarded",
5
+ "cf-connecting-ip",
6
+ "true-client-ip",
7
+ "x-client-ip",
8
+ "x-cluster-client-ip"
9
+ ];
10
+ export const FINGERPRINT_HEADERS = [
11
+ "user-agent",
12
+ "accept-language",
13
+ "accept-encoding",
14
+ "sec-ch-ua",
15
+ "sec-ch-ua-platform",
16
+ "sec-ch-ua-mobile",
17
+ "sec-ch-ua-full-version-list"
18
+ ];
19
+ export const SENSITIVE_HEADERS = [
20
+ "cookie",
21
+ "authorization",
22
+ "proxy-authorization",
23
+ "x-csrf-token",
24
+ "www-authenticate"
25
+ ];
26
+ export const STRIP_PARAMS = {
27
+ // IP addresses — anonymized to subnet
28
+ ip: ["uip", "ip", "client_ip_address", "ip_address", "user_ip", "ipaddress", "context.ip"],
29
+ // User identifiers — intentionally preserved for analytics functionality
30
+ userId: ["uid", "user_id", "userid", "external_id", "cid", "_gid", "fbp", "fbc", "sid", "session_id", "sessionid", "pl_id", "p_user_id", "uuid", "anonymousid", "twclid", "u_c1", "u_sclid", "u_scsid"],
31
+ // User data (PII) — intentionally preserved; hashed by analytics SDKs before sending
32
+ userData: ["ud", "user_data", "userdata", "email", "phone", "traits.email", "traits.phone"],
33
+ // Screen/Hardware — generalized to common buckets
34
+ screen: ["sr", "vp", "sd", "screen", "viewport", "colordepth", "pixelratio", "sh", "sw"],
35
+ // Hardware capabilities — generalized to common buckets
36
+ hardware: ["hardwareconcurrency", "devicememory", "cpu", "mem"],
37
+ // Platform identifiers — low entropy, kept as-is (e.g. "Linux", "x86")
38
+ platform: ["plat", "platform", "d_a", "d_ot"],
39
+ // Version strings — generalized to major version only (d_os = Snapchat OS version, uapv = GA platform version)
40
+ version: ["d_os", "uapv"],
41
+ // Browser version lists — generalized to major versions (d_bvs = Snapchat, uafvl = GA Client Hints)
42
+ browserVersion: ["d_bvs", "uafvl"],
43
+ // Browser data lists — replaced with empty value
44
+ browserData: ["plugins", "fonts"],
45
+ // Location/Timezone — generalized
46
+ location: ["tz", "timezone", "timezoneoffset"],
47
+ // Canvas/WebGL/Audio fingerprints — replaced with empty value (pure fingerprints, no analytics value)
48
+ canvas: ["canvas", "webgl", "audiofingerprint"],
49
+ // Combined device fingerprinting (X/Twitter dv param contains: timezone, locale, vendor, platform, screen, etc.)
50
+ deviceInfo: ["dv", "device_info", "deviceinfo"]
51
+ };
52
+ export const NORMALIZE_PARAMS = {
53
+ language: ["ul", "lang", "language", "languages"],
54
+ userAgent: ["ua", "useragent", "user_agent", "client_user_agent", "context.useragent"]
55
+ };
56
+ export function anonymizeIP(ip) {
57
+ if (ip.includes(":")) {
58
+ return ip.split(":").slice(0, 3).join(":") + "::";
59
+ }
60
+ const parts = ip.split(".");
61
+ if (parts.length === 4) {
62
+ parts[3] = "0";
63
+ return parts.join(".");
64
+ }
65
+ return ip;
66
+ }
67
+ export function normalizeUserAgent(ua) {
68
+ const tokens = [
69
+ ["Edg/", "Edge"],
70
+ ["OPR/", "Opera"],
71
+ ["Opera/", "Opera"],
72
+ ["Firefox/", "Firefox"],
73
+ ["Chrome/", "Chrome"],
74
+ ["Safari/", "Safari"]
75
+ ];
76
+ for (const [pattern, family] of tokens) {
77
+ const idx = ua.indexOf(pattern);
78
+ if (idx !== -1) {
79
+ const versionStart = idx + pattern.length;
80
+ const majorVersion = ua.slice(versionStart).match(/^(\d+)/)?.[1];
81
+ if (majorVersion)
82
+ return `Mozilla/5.0 (compatible; ${family}/${majorVersion}.0)`;
83
+ }
84
+ }
85
+ return "Mozilla/5.0 (compatible)";
86
+ }
87
+ export function normalizeLanguage(lang) {
88
+ return lang.split(",")[0]?.split(";")[0]?.trim() || "en";
89
+ }
90
+ const SCREEN_BUCKETS = {
91
+ desktop: { w: 1920, h: 1080 },
92
+ tablet: { w: 768, h: 1024 },
93
+ mobile: { w: 360, h: 640 }
94
+ };
95
+ function getDeviceClass(width) {
96
+ if (width >= 1200) return "desktop";
97
+ if (width >= 700) return "tablet";
98
+ return "mobile";
99
+ }
100
+ export function generalizeScreen(value, dimension) {
101
+ if (typeof value === "string" && value.includes("x")) {
102
+ const width = Number.parseInt(value.split("x")[0] || "0");
103
+ const cls = getDeviceClass(width);
104
+ return `${SCREEN_BUCKETS[cls].w}x${SCREEN_BUCKETS[cls].h}`;
105
+ }
106
+ const num = typeof value === "number" ? value : Number(value);
107
+ if (!Number.isNaN(num)) {
108
+ const cls = getDeviceClass(num);
109
+ const bucketed = dimension === "height" ? SCREEN_BUCKETS[cls].h : SCREEN_BUCKETS[cls].w;
110
+ return typeof value === "number" ? bucketed : String(bucketed);
111
+ }
112
+ return "1920x1080";
113
+ }
114
+ export function generalizeHardware(value) {
115
+ const num = typeof value === "number" ? value : Number(value);
116
+ if (Number.isNaN(num)) return 4;
117
+ if (num >= 16) return 16;
118
+ if (num >= 8) return 8;
119
+ if (num >= 4) return 4;
120
+ return 2;
121
+ }
122
+ export function generalizeVersion(value) {
123
+ if (typeof value !== "string") return String(value);
124
+ const match = value.match(/^(\d+)(([.\-_])\d+)*/);
125
+ if (!match) return String(value);
126
+ const major = match[1];
127
+ const sep = match[3] || ".";
128
+ const segmentCount = value.split(/[.\-_]/).length;
129
+ return major + (sep + "0").repeat(segmentCount - 1);
130
+ }
131
+ export function generalizeBrowserVersions(value) {
132
+ if (typeof value !== "string") return String(value);
133
+ const zeroSegments = (ver) => {
134
+ const parts = ver.split(".");
135
+ return parts[0] + parts.slice(1).map(() => ".0").join("");
136
+ };
137
+ if (value.includes('"version"'))
138
+ return value.replace(/("version"\s*:\s*")(\d+(?:\.\d+)*)/g, (_, prefix, ver) => prefix + zeroSegments(ver));
139
+ if (value.includes(";"))
140
+ return value.replace(/;(\d+(?:\.\d+)*)/g, (_, ver) => ";" + zeroSegments(ver));
141
+ return value;
142
+ }
143
+ export function generalizeTimezone(value) {
144
+ if (typeof value === "number") {
145
+ return Math.round(value / 180) * 180;
146
+ }
147
+ if (typeof value === "string") {
148
+ return "UTC";
149
+ }
150
+ return 0;
151
+ }
152
+ export function anonymizeDeviceInfo(value) {
153
+ const sep = value.includes("|") ? "|" : "&";
154
+ const parts = value.split(sep);
155
+ if (parts.length < 4) return value;
156
+ const result = [...parts];
157
+ for (let i = 0; i < parts.length; i++) {
158
+ const part = parts[i];
159
+ if (part.includes("/") && /^[A-Z]/.test(part)) {
160
+ result[i] = String(generalizeTimezone(part));
161
+ continue;
162
+ }
163
+ if (/^[a-z]{2}(?:-[a-z]{2,})?$/i.test(part)) {
164
+ result[i] = normalizeLanguage(part);
165
+ continue;
166
+ }
167
+ const num = Number(part);
168
+ if (!Number.isNaN(num) && num >= 300 && num <= 1e4) {
169
+ const nextNum = Number(parts[i + 1]);
170
+ if (!Number.isNaN(nextNum) && nextNum >= 300 && nextNum <= 1e4) {
171
+ const cls = getDeviceClass(num);
172
+ result[i] = String(SCREEN_BUCKETS[cls].w);
173
+ result[i + 1] = String(SCREEN_BUCKETS[cls].h);
174
+ i++;
175
+ continue;
176
+ }
177
+ result[i] = String(generalizeScreen(num));
178
+ continue;
179
+ }
180
+ if (!Number.isNaN(num) && num < -60) {
181
+ result[i] = String(generalizeTimezone(num));
182
+ }
183
+ }
184
+ return result.join(sep);
185
+ }
186
+ export function stripPayloadFingerprinting(payload) {
187
+ const result = {};
188
+ let deviceClass;
189
+ for (const [key, value] of Object.entries(payload)) {
190
+ if (key.toLowerCase() === "sw") {
191
+ const num = typeof value === "number" ? value : Number(value);
192
+ if (!Number.isNaN(num)) deviceClass = getDeviceClass(num);
193
+ }
194
+ }
195
+ for (const [key, value] of Object.entries(payload)) {
196
+ const lowerKey = key.toLowerCase();
197
+ const isLanguageParam = NORMALIZE_PARAMS.language.some((p) => lowerKey === p.toLowerCase());
198
+ const isUserAgentParam = NORMALIZE_PARAMS.userAgent.some((p) => lowerKey === p.toLowerCase());
199
+ if ((isLanguageParam || isUserAgentParam) && typeof value === "string") {
200
+ result[key] = isLanguageParam ? normalizeLanguage(value) : normalizeUserAgent(value);
201
+ continue;
202
+ }
203
+ const matchesParam = (key2, params) => {
204
+ const lk = key2.toLowerCase();
205
+ return params.some((p) => {
206
+ const lp = p.toLowerCase();
207
+ return lk === lp || lk.startsWith(lp + "[");
208
+ });
209
+ };
210
+ if (matchesParam(key, STRIP_PARAMS.ip) && typeof value === "string") {
211
+ result[key] = anonymizeIP(value);
212
+ continue;
213
+ }
214
+ if (matchesParam(key, STRIP_PARAMS.screen)) {
215
+ if (["sd", "colordepth", "pixelratio"].includes(lowerKey)) {
216
+ result[key] = value;
217
+ } else if (lowerKey === "sh" && deviceClass) {
218
+ const paired = SCREEN_BUCKETS[deviceClass].h;
219
+ result[key] = typeof value === "number" ? paired : String(paired);
220
+ } else {
221
+ result[key] = generalizeScreen(value, lowerKey === "sw" ? "width" : lowerKey === "sh" ? "height" : void 0);
222
+ }
223
+ continue;
224
+ }
225
+ if (matchesParam(key, STRIP_PARAMS.hardware)) {
226
+ result[key] = generalizeHardware(value);
227
+ continue;
228
+ }
229
+ if (matchesParam(key, STRIP_PARAMS.version)) {
230
+ result[key] = generalizeVersion(value);
231
+ continue;
232
+ }
233
+ if (matchesParam(key, STRIP_PARAMS.browserVersion)) {
234
+ result[key] = generalizeBrowserVersions(value);
235
+ continue;
236
+ }
237
+ if (matchesParam(key, STRIP_PARAMS.location)) {
238
+ result[key] = generalizeTimezone(value);
239
+ continue;
240
+ }
241
+ if (matchesParam(key, STRIP_PARAMS.browserData)) {
242
+ result[key] = Array.isArray(value) ? [] : "";
243
+ continue;
244
+ }
245
+ if (matchesParam(key, STRIP_PARAMS.canvas)) {
246
+ result[key] = typeof value === "number" ? 0 : typeof value === "object" ? {} : "";
247
+ continue;
248
+ }
249
+ if (matchesParam(key, STRIP_PARAMS.deviceInfo)) {
250
+ result[key] = typeof value === "string" ? anonymizeDeviceInfo(value) : "";
251
+ continue;
252
+ }
253
+ if (matchesParam(key, STRIP_PARAMS.platform)) {
254
+ result[key] = value;
255
+ continue;
256
+ }
257
+ if (Array.isArray(value)) {
258
+ result[key] = value.map(
259
+ (item) => typeof item === "object" && item !== null ? stripPayloadFingerprinting(item) : item
260
+ );
261
+ } else if (typeof value === "object" && value !== null) {
262
+ result[key] = stripPayloadFingerprinting(value);
263
+ } else {
264
+ result[key] = value;
265
+ }
266
+ }
267
+ return result;
268
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
+ export default _default;
@@ -0,0 +1,53 @@
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;
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
+ const allowedDomains = [
28
+ "pbs.twimg.com",
29
+ "abs.twimg.com",
30
+ "video.twimg.com"
31
+ ];
32
+ if (!allowedDomains.includes(parsedUrl.hostname)) {
33
+ throw createError({
34
+ statusCode: 403,
35
+ statusMessage: "Domain not allowed"
36
+ });
37
+ }
38
+ const response = await $fetch.raw(url, {
39
+ timeout: 5e3,
40
+ headers: {
41
+ "Accept": "image/webp,image/jpeg,image/png,image/*,*/*;q=0.8",
42
+ "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"
43
+ }
44
+ }).catch((error) => {
45
+ throw createError({
46
+ statusCode: error.statusCode || 500,
47
+ statusMessage: error.statusMessage || "Failed to fetch image"
48
+ });
49
+ });
50
+ setHeader(event, "Content-Type", response.headers.get("content-type") || "image/jpeg");
51
+ setHeader(event, "Cache-Control", "public, max-age=3600, s-maxage=3600");
52
+ return response._data;
53
+ });