@nuxt/scripts 1.0.0-rc.8 → 1.0.0

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 (97) hide show
  1. package/dist/devtools-client/200.html +1 -1
  2. package/dist/devtools-client/404.html +1 -1
  3. package/dist/devtools-client/_nuxt/{BBS9G2Kb.js → Br5kvbNb.js} +1 -1
  4. package/dist/devtools-client/_nuxt/C25MBdR1.js +1 -0
  5. package/dist/devtools-client/_nuxt/{DCBsJT4N.js → Cg_OIb5q.js} +1 -1
  6. package/dist/devtools-client/_nuxt/{B4uHpJPz.js → D2o5loaz.js} +1 -1
  7. package/dist/devtools-client/_nuxt/De7Wf2b9.js +188 -0
  8. package/dist/devtools-client/_nuxt/{Cxq4HLPL.js → DnVCfhVR.js} +1 -1
  9. package/dist/devtools-client/_nuxt/builds/latest.json +1 -1
  10. package/dist/devtools-client/_nuxt/builds/meta/9d868e70-bc5a-425c-8c84-8defe5186920.json +1 -0
  11. package/dist/devtools-client/_nuxt/{entry.BwpOBArY.css → entry.BSxy0W1q.css} +1 -1
  12. package/dist/devtools-client/_nuxt/index.DZD1lwyI.css +1 -0
  13. package/dist/devtools-client/_nuxt/{DvZScWzI.js → pN4-T8ZD.js} +1 -1
  14. package/dist/devtools-client/docs/index.html +1 -1
  15. package/dist/devtools-client/first-party/index.html +1 -1
  16. package/dist/devtools-client/index.html +1 -1
  17. package/dist/devtools-client/registry/index.html +1 -1
  18. package/dist/module.d.mts +15 -0
  19. package/dist/module.d.ts +15 -0
  20. package/dist/module.json +1 -1
  21. package/dist/module.mjs +35 -8
  22. package/dist/registry.mjs +3 -3
  23. package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsStaticMap.vue +6 -2
  24. package/dist/runtime/components/ScriptBlueskyEmbed.d.vue.ts +0 -1
  25. package/dist/runtime/components/ScriptBlueskyEmbed.vue +12 -10
  26. package/dist/runtime/components/ScriptBlueskyEmbed.vue.d.ts +0 -1
  27. package/dist/runtime/components/ScriptInstagramEmbed.vue +3 -1
  28. package/dist/runtime/components/ScriptXEmbed.d.vue.ts +0 -1
  29. package/dist/runtime/components/ScriptXEmbed.vue +11 -9
  30. package/dist/runtime/components/ScriptXEmbed.vue.d.ts +0 -1
  31. package/dist/runtime/composables/useScript.js +17 -6
  32. package/dist/runtime/composables/useScriptProxyToken.d.ts +12 -0
  33. package/dist/runtime/composables/useScriptProxyToken.js +4 -0
  34. package/dist/runtime/composables/useScriptProxyUrl.d.ts +12 -0
  35. package/dist/runtime/composables/useScriptProxyUrl.js +27 -0
  36. package/dist/runtime/plugins/proxy-token.server.d.ts +10 -0
  37. package/dist/runtime/plugins/proxy-token.server.js +17 -0
  38. package/dist/runtime/registry/bing-uet.d.ts +189 -11
  39. package/dist/runtime/registry/bing-uet.js +16 -2
  40. package/dist/runtime/registry/bluesky-embed.d.ts +0 -4
  41. package/dist/runtime/registry/bluesky-embed.js +0 -4
  42. package/dist/runtime/registry/clarity.d.ts +6 -2
  43. package/dist/runtime/registry/clarity.js +12 -1
  44. package/dist/runtime/registry/google-analytics.d.ts +6 -2
  45. package/dist/runtime/registry/google-analytics.js +12 -1
  46. package/dist/runtime/registry/google-tag-manager.d.ts +6 -2
  47. package/dist/runtime/registry/google-tag-manager.js +10 -1
  48. package/dist/runtime/registry/gravatar.js +10 -13
  49. package/dist/runtime/registry/matomo-analytics.d.ts +9 -3
  50. package/dist/runtime/registry/matomo-analytics.js +28 -1
  51. package/dist/runtime/registry/meta-pixel.d.ts +8 -2
  52. package/dist/runtime/registry/meta-pixel.js +10 -1
  53. package/dist/runtime/registry/mixpanel-analytics.d.ts +12 -2
  54. package/dist/runtime/registry/mixpanel-analytics.js +16 -4
  55. package/dist/runtime/registry/posthog.d.ts +8 -2
  56. package/dist/runtime/registry/posthog.js +15 -4
  57. package/dist/runtime/registry/schemas.d.ts +65 -0
  58. package/dist/runtime/registry/schemas.js +75 -8
  59. package/dist/runtime/registry/tiktok-pixel.d.ts +16 -2
  60. package/dist/runtime/registry/tiktok-pixel.js +22 -1
  61. package/dist/runtime/registry/x-embed.d.ts +0 -4
  62. package/dist/runtime/registry/x-embed.js +0 -4
  63. package/dist/runtime/server/bluesky-embed-image.d.ts +1 -1
  64. package/dist/runtime/server/bluesky-embed.d.ts +1 -15
  65. package/dist/runtime/server/bluesky-embed.js +22 -4
  66. package/dist/runtime/server/google-maps-geocode-proxy.js +8 -5
  67. package/dist/runtime/server/google-static-maps-proxy.d.ts +1 -1
  68. package/dist/runtime/server/google-static-maps-proxy.js +13 -8
  69. package/dist/runtime/server/gravatar-proxy.d.ts +1 -1
  70. package/dist/runtime/server/gravatar-proxy.js +6 -7
  71. package/dist/runtime/server/instagram-embed-asset.d.ts +1 -1
  72. package/dist/runtime/server/instagram-embed-image.d.ts +1 -1
  73. package/dist/runtime/server/instagram-embed.js +22 -10
  74. package/dist/runtime/server/utils/cached-upstream.d.ts +55 -0
  75. package/dist/runtime/server/utils/cached-upstream.js +65 -0
  76. package/dist/runtime/server/utils/embed-rewriters.d.ts +19 -0
  77. package/dist/runtime/server/utils/embed-rewriters.js +41 -0
  78. package/dist/runtime/server/utils/image-proxy.d.ts +3 -1
  79. package/dist/runtime/server/utils/image-proxy.js +8 -6
  80. package/dist/runtime/server/utils/instagram-embed.d.ts +4 -4
  81. package/dist/runtime/server/utils/instagram-embed.js +10 -9
  82. package/dist/runtime/server/utils/proxy-url.d.ts +9 -0
  83. package/dist/runtime/server/utils/proxy-url.js +21 -0
  84. package/dist/runtime/server/utils/sign-constants.d.ts +16 -0
  85. package/dist/runtime/server/utils/sign-constants.js +5 -0
  86. package/dist/runtime/server/utils/sign.d.ts +2 -10
  87. package/dist/runtime/server/utils/sign.js +8 -5
  88. package/dist/runtime/server/utils/withSigning.js +3 -2
  89. package/dist/runtime/server/x-embed-image.d.ts +1 -1
  90. package/dist/runtime/server/x-embed.js +20 -2
  91. package/dist/runtime/types.d.ts +24 -1
  92. package/dist/types-source.mjs +160 -12
  93. package/package.json +2 -2
  94. package/dist/devtools-client/_nuxt/CQR4zIAm.js +0 -1
  95. package/dist/devtools-client/_nuxt/DTxy5P8N.js +0 -188
  96. package/dist/devtools-client/_nuxt/builds/meta/484f72b9-a019-4127-8ab9-c10e92624094.json +0 -1
  97. package/dist/devtools-client/_nuxt/index.CA-OpSj0.css +0 -1
@@ -1,11 +1,22 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
- import { $fetch } from "ofetch";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
3
  import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from "ultrahtml";
4
- import { rewriteUrl, rewriteUrlsInText, RSRC_RE, scopeCss } from "./utils/instagram-embed.js";
4
+ import { createCachedJsonFetch } from "./utils/cached-upstream.js";
5
+ import { proxyAssetUrl, rewriteUrl, rewriteUrlsInText, RSRC_RE, scopeCss } from "./utils/instagram-embed.js";
5
6
  import { withSigning } from "./utils/withSigning.js";
6
7
  export { proxyAssetUrl, proxyImageUrl, rewriteUrl, rewriteUrlsInText, scopeCss } from "./utils/instagram-embed.js";
7
8
  const EMBED_INSTAGRAM_SUFFIX_RE = /\/embed\/instagram$/;
8
9
  const SRCSET_SPLIT_RE = /\s+/;
10
+ const cachedEmbedFetch = createCachedJsonFetch(
11
+ "nuxt-scripts-instagram-embed",
12
+ 600,
13
+ (url) => url
14
+ );
15
+ const cachedCssFetch = createCachedJsonFetch(
16
+ "nuxt-scripts-instagram-css",
17
+ 86400,
18
+ (url) => url
19
+ );
9
20
  function removeNode(node) {
10
21
  node.type = TEXT_NODE;
11
22
  node.value = "";
@@ -16,6 +27,7 @@ function removeNode(node) {
16
27
  export default withSigning(defineEventHandler(async (event) => {
17
28
  const handlerPath = event.path?.split("?")[0] || "";
18
29
  const prefix = handlerPath.replace(EMBED_INSTAGRAM_SUFFIX_RE, "") || "/_scripts";
30
+ const secret = useRuntimeConfig(event)["nuxt-scripts"]?.proxySecret;
19
31
  const query = getQuery(event);
20
32
  const postUrl = query.url;
21
33
  const captions = query.captions === "true";
@@ -43,7 +55,7 @@ export default withSigning(defineEventHandler(async (event) => {
43
55
  const pathname = parsedUrl.pathname.endsWith("/") ? parsedUrl.pathname : `${parsedUrl.pathname}/`;
44
56
  const cleanUrl = parsedUrl.origin + pathname;
45
57
  const embedUrl = `${cleanUrl}embed/${captions ? "captioned/" : ""}`;
46
- const html = await $fetch(embedUrl, {
58
+ const html = await cachedEmbedFetch(embedUrl, {
47
59
  headers: {
48
60
  "Accept": "text/html",
49
61
  "User-Agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
@@ -70,22 +82,22 @@ export default withSigning(defineEventHandler(async (event) => {
70
82
  }
71
83
  for (const attr of ["src", "poster"]) {
72
84
  if (node.attributes[attr])
73
- node.attributes[attr] = rewriteUrl(node.attributes[attr], prefix);
85
+ node.attributes[attr] = rewriteUrl(node.attributes[attr], prefix, secret);
74
86
  }
75
87
  if (node.attributes.srcset) {
76
88
  node.attributes.srcset = node.attributes.srcset.split(",").map((entry) => {
77
89
  const parts = entry.trim().split(SRCSET_SPLIT_RE);
78
90
  const url = parts[0];
79
91
  const descriptor = parts.slice(1).join(" ");
80
- return url ? `${rewriteUrl(url, prefix)}${descriptor ? ` ${descriptor}` : ""}` : entry;
92
+ return url ? `${rewriteUrl(url, prefix, secret)}${descriptor ? ` ${descriptor}` : ""}` : entry;
81
93
  }).join(", ");
82
94
  }
83
95
  if (node.attributes.style)
84
- node.attributes.style = rewriteUrlsInText(node.attributes.style, prefix);
96
+ node.attributes.style = rewriteUrlsInText(node.attributes.style, prefix, secret);
85
97
  });
86
98
  walkSync(ast, (node) => {
87
99
  if (node.type === TEXT_NODE && node.value)
88
- node.value = rewriteUrlsInText(node.value, prefix);
100
+ node.value = rewriteUrlsInText(node.value, prefix, secret);
89
101
  });
90
102
  let bodyNode = null;
91
103
  walkSync(ast, (node) => {
@@ -95,7 +107,7 @@ export default withSigning(defineEventHandler(async (event) => {
95
107
  const bodyHtml = bodyNode ? bodyNode.children.map((child) => renderSync(child)).join("") : renderSync(ast);
96
108
  const cssContents = await Promise.all(
97
109
  cssUrls.map(
98
- (url) => $fetch(url, {
110
+ (url) => cachedCssFetch(url, {
99
111
  headers: { Accept: "text/css" }
100
112
  }).catch(() => "")
101
113
  )
@@ -103,9 +115,9 @@ export default withSigning(defineEventHandler(async (event) => {
103
115
  let combinedCss = cssContents.join("\n");
104
116
  combinedCss = combinedCss.replace(
105
117
  RSRC_RE,
106
- (_m, path) => `url(${prefix}/embed/instagram-asset?url=${encodeURIComponent(`https://static.cdninstagram.com/rsrc.php${path}`)})`
118
+ (_m, path) => `url(${proxyAssetUrl(`https://static.cdninstagram.com/rsrc.php${path}`, prefix, secret)})`
107
119
  );
108
- combinedCss = rewriteUrlsInText(combinedCss, prefix);
120
+ combinedCss = rewriteUrlsInText(combinedCss, prefix, secret);
109
121
  combinedCss = scopeCss(combinedCss, ".instagram-embed-root");
110
122
  const baseStyles = `
111
123
  .instagram-embed-root { 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; }
@@ -0,0 +1,55 @@
1
+ import { Buffer } from 'node:buffer';
2
+ /**
3
+ * Server-side caches for upstream proxy fetches.
4
+ *
5
+ * ## Why
6
+ *
7
+ * Proxy URLs arriving from the client carry per-request auth artefacts (`sig`,
8
+ * `_pt`, `_ts`) that change across renders. CDNs key on full URL so each
9
+ * rotation produces a unique edge cache entry and upstream origins take the hit
10
+ * on every render. Caching the *upstream response* here — keyed on the inner
11
+ * resource URL (or normalized param set) — dedupes those fetches across every
12
+ * request that resolves to the same upstream, regardless of how the caller
13
+ * authenticated.
14
+ *
15
+ * Safe because `withSigning` runs before any cache path: unsigned requests 403
16
+ * before they can do a cache lookup. Cache stores hold only responses produced
17
+ * from legitimately-authenticated requests.
18
+ *
19
+ * ## Binary payloads
20
+ *
21
+ * Image/blob responses are stored as base64 strings so they round-trip cleanly
22
+ * through every unstorage driver (memory, filesystem, redis, cloudflare kv).
23
+ * The 33% size overhead is tolerable; the alternative is relying on each driver
24
+ * to preserve Buffer/ArrayBuffer which is not universal.
25
+ */
26
+ export interface CachedBinaryResponse {
27
+ base64: string;
28
+ contentType: string | null;
29
+ }
30
+ export interface CachedBinaryFetchOptions {
31
+ headers?: Record<string, string>;
32
+ timeout?: number;
33
+ redirect?: 'follow' | 'manual';
34
+ ignoreResponseError?: boolean;
35
+ }
36
+ export interface CachedBinaryResult extends CachedBinaryResponse {
37
+ body: Buffer;
38
+ status: number;
39
+ }
40
+ /**
41
+ * Cache upstream binary/image fetches. Returns a helper that restores the
42
+ * response body as a Buffer so the handler can pipe it straight to the client.
43
+ */
44
+ export declare function createCachedBinaryFetch(name: string, maxAge: number): (url: string, opts?: CachedBinaryFetchOptions) => Promise<CachedBinaryResult>;
45
+ /**
46
+ * Cache upstream JSON/text fetches. `getKey` is caller-controlled so handlers
47
+ * can normalize on whichever inner params identify the resource (tweet ID,
48
+ * post URL, query hash).
49
+ */
50
+ export declare function createCachedJsonFetch<T>(name: string, maxAge: number, getKey: (url: string, opts?: {
51
+ headers?: Record<string, string>;
52
+ }) => string): (url: string, opts?: {
53
+ headers?: Record<string, string>;
54
+ timeout?: number;
55
+ }) => Promise<T>;
@@ -0,0 +1,65 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { defineCachedFunction } from "nitropack/runtime";
3
+ import { $fetch } from "ofetch";
4
+ export function createCachedBinaryFetch(name, maxAge) {
5
+ const cached = defineCachedFunction(
6
+ async (url, opts) => {
7
+ const response = await $fetch.raw(url, {
8
+ responseType: "arrayBuffer",
9
+ timeout: opts?.timeout ?? 1e4,
10
+ redirect: opts?.redirect ?? "follow",
11
+ ignoreResponseError: opts?.ignoreResponseError ?? false,
12
+ headers: opts?.headers
13
+ });
14
+ const data = response._data;
15
+ return {
16
+ base64: data ? Buffer.from(data).toString("base64") : "",
17
+ contentType: response.headers.get("content-type"),
18
+ status: response.status
19
+ };
20
+ },
21
+ {
22
+ name,
23
+ maxAge,
24
+ swr: true,
25
+ staleMaxAge: maxAge,
26
+ getKey: (url, opts) => {
27
+ if (!opts)
28
+ return url;
29
+ const parts = [url];
30
+ if (opts.headers) {
31
+ const entries = Object.entries(opts.headers).sort(([a], [b]) => a.localeCompare(b));
32
+ for (const [k, v] of entries)
33
+ parts.push(`${k}=${v}`);
34
+ }
35
+ if (opts.redirect)
36
+ parts.push(`redirect=${opts.redirect}`);
37
+ return parts.join("|");
38
+ }
39
+ }
40
+ );
41
+ return async (url, opts) => {
42
+ const result = await cached(url, opts);
43
+ return {
44
+ ...result,
45
+ body: result.base64 ? Buffer.from(result.base64, "base64") : Buffer.alloc(0)
46
+ };
47
+ };
48
+ }
49
+ export function createCachedJsonFetch(name, maxAge, getKey) {
50
+ return defineCachedFunction(
51
+ async (url, opts) => {
52
+ return await $fetch(url, {
53
+ timeout: opts?.timeout ?? 1e4,
54
+ headers: opts?.headers
55
+ });
56
+ },
57
+ {
58
+ name,
59
+ maxAge,
60
+ swr: true,
61
+ staleMaxAge: maxAge,
62
+ getKey
63
+ }
64
+ );
65
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Mutate a tweet (and any quoted tweet) in place so every raw CDN image URL
3
+ * is rewritten to route through the site's `/embed/x-image` proxy. When a
4
+ * `secret` is provided, URLs are HMAC-signed and pass `withSigning` without a
5
+ * page token.
6
+ *
7
+ * Clone the input first if it came from a shared cache — this function does
8
+ * not copy.
9
+ */
10
+ export declare function rewriteTweetImages(tweet: any, imagePath: string, secret?: string): void;
11
+ /**
12
+ * Mutate a Bluesky post in place so every CDN image URL routes through the
13
+ * site's `/embed/bluesky-image` proxy. Covers author avatar, embedded images
14
+ * (thumb + fullsize), and external embed thumbnails.
15
+ *
16
+ * Clone the input first if it came from a shared cache — this function does
17
+ * not copy.
18
+ */
19
+ export declare function rewriteBlueskyPostImages(post: any, imagePath: string, secret?: string): void;
@@ -0,0 +1,41 @@
1
+ import { buildProxyUrl } from "./proxy-url.js";
2
+ export function rewriteTweetImages(tweet, imagePath, secret) {
3
+ if (!tweet)
4
+ return;
5
+ if (tweet.user?.profile_image_url_https)
6
+ tweet.user.profile_image_url_https = buildProxyUrl(imagePath, { url: tweet.user.profile_image_url_https }, secret);
7
+ if (tweet.photos) {
8
+ for (const photo of tweet.photos) {
9
+ if (photo.url)
10
+ photo.url = buildProxyUrl(imagePath, { url: photo.url }, secret);
11
+ }
12
+ }
13
+ if (tweet.entities?.media) {
14
+ for (const media of tweet.entities.media) {
15
+ if (media.media_url_https)
16
+ media.media_url_https = buildProxyUrl(imagePath, { url: media.media_url_https }, secret);
17
+ }
18
+ }
19
+ if (tweet.video?.poster)
20
+ tweet.video.poster = buildProxyUrl(imagePath, { url: tweet.video.poster }, secret);
21
+ if (tweet.quoted_tweet)
22
+ rewriteTweetImages(tweet.quoted_tweet, imagePath, secret);
23
+ }
24
+ export function rewriteBlueskyPostImages(post, imagePath, secret) {
25
+ if (!post)
26
+ return;
27
+ const proxy = (url) => url ? buildProxyUrl(imagePath, { url }, secret) : url;
28
+ if (post.author?.avatar)
29
+ post.author.avatar = proxy(post.author.avatar);
30
+ const embed = post.embed;
31
+ if (embed?.images) {
32
+ for (const image of embed.images) {
33
+ if (image.thumb)
34
+ image.thumb = proxy(image.thumb);
35
+ if (image.fullsize)
36
+ image.fullsize = proxy(image.fullsize);
37
+ }
38
+ }
39
+ if (embed?.external?.thumb)
40
+ embed.external.thumb = proxy(embed.external.thumb);
41
+ }
@@ -8,5 +8,7 @@ export interface ImageProxyConfig {
8
8
  followRedirects?: boolean;
9
9
  /** Decode &amp; in URL query parameter */
10
10
  decodeAmpersands?: boolean;
11
+ /** Unique name for the nitro cache group (defaults to derived from allowedDomains). */
12
+ cacheName?: string;
11
13
  }
12
- export declare function createImageProxyHandler(config: ImageProxyConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
14
+ export declare function createImageProxyHandler(config: ImageProxyConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Buffer<ArrayBufferLike>>>;
@@ -1,5 +1,5 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
- import { $fetch } from "ofetch";
2
+ import { createCachedBinaryFetch } from "./cached-upstream.js";
3
3
  import { withSigning } from "./withSigning.js";
4
4
  const AMP_RE = /&amp;/g;
5
5
  export function createImageProxyHandler(config) {
@@ -9,8 +9,10 @@ export function createImageProxyHandler(config) {
9
9
  cacheMaxAge = 3600,
10
10
  contentType = "image/jpeg",
11
11
  followRedirects = true,
12
- decodeAmpersands = false
12
+ decodeAmpersands = false,
13
+ cacheName = Array.isArray(config.allowedDomains) ? `nuxt-scripts-img:${config.allowedDomains[0] || "default"}` : "nuxt-scripts-img:custom"
13
14
  } = config;
15
+ const cachedFetch = createCachedBinaryFetch(cacheName, cacheMaxAge);
14
16
  return withSigning(defineEventHandler(async (event) => {
15
17
  const query = getQuery(event);
16
18
  let url = query.url;
@@ -47,7 +49,7 @@ export function createImageProxyHandler(config) {
47
49
  const headers = { Accept: accept };
48
50
  if (userAgent)
49
51
  headers["User-Agent"] = userAgent;
50
- const response = await $fetch.raw(url, {
52
+ const result = await cachedFetch(url, {
51
53
  timeout: 5e3,
52
54
  redirect: followRedirects ? "follow" : "manual",
53
55
  ignoreResponseError: !followRedirects,
@@ -58,14 +60,14 @@ export function createImageProxyHandler(config) {
58
60
  statusMessage: error.statusMessage || "Failed to fetch image"
59
61
  });
60
62
  });
61
- if (!followRedirects && response.status >= 300 && response.status < 400) {
63
+ if (!followRedirects && result.status >= 300 && result.status < 400) {
62
64
  throw createError({
63
65
  statusCode: 403,
64
66
  statusMessage: "Redirects not allowed"
65
67
  });
66
68
  }
67
- setHeader(event, "Content-Type", response.headers.get("content-type") || contentType);
69
+ setHeader(event, "Content-Type", result.contentType || contentType);
68
70
  setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
69
- return response._data;
71
+ return result.body;
70
72
  }));
71
73
  }
@@ -5,10 +5,10 @@ export declare const STATIC_CDN_RE: RegExp;
5
5
  export declare const LOOKASIDE_RE: RegExp;
6
6
  export declare const INSTAGRAM_IMAGE_HOSTS: string[];
7
7
  export declare const INSTAGRAM_ASSET_HOST = "static.cdninstagram.com";
8
- export declare function proxyImageUrl(url: string, prefix?: string): string;
9
- export declare function proxyAssetUrl(url: string, prefix?: string): string;
10
- export declare function rewriteUrl(url: string, prefix?: string): string;
11
- export declare function rewriteUrlsInText(text: string, prefix?: string): string;
8
+ export declare function proxyImageUrl(url: string, prefix?: string, secret?: string): string;
9
+ export declare function proxyAssetUrl(url: string, prefix?: string, secret?: string): string;
10
+ export declare function rewriteUrl(url: string, prefix?: string, secret?: string): string;
11
+ export declare function rewriteUrlsInText(text: string, prefix?: string, secret?: string): string;
12
12
  /**
13
13
  * Scope CSS rules under a parent selector and strip global/page-level rules.
14
14
  * Removes :root, html, body selectors and @charset/@import at-rules.
@@ -1,3 +1,4 @@
1
+ import { buildProxyUrl } from "./proxy-url.js";
1
2
  export const RSRC_RE = /url\(\/rsrc\.php([^)]+)\)/g;
2
3
  export const AMP_RE = /&amp;/g;
3
4
  export const SCONTENT_RE = /https:\/\/scontent[^"'\s),]+\.cdninstagram\.com[^"'\s),]+/g;
@@ -10,25 +11,25 @@ const IMPORT_RE = /@import\s[^;]+;/gi;
10
11
  const WHITESPACE_RE = /\s/;
11
12
  const AT_RULE_NAME_RE = /@([\w-]+)/;
12
13
  const MULTI_SPACE_RE = /\s+/g;
13
- export function proxyImageUrl(url, prefix = "/_scripts") {
14
- return `${prefix}/embed/instagram-image?url=${encodeURIComponent(url.replace(AMP_RE, "&"))}`;
14
+ export function proxyImageUrl(url, prefix = "/_scripts", secret) {
15
+ return buildProxyUrl(`${prefix}/embed/instagram-image`, { url: url.replace(AMP_RE, "&") }, secret);
15
16
  }
16
- export function proxyAssetUrl(url, prefix = "/_scripts") {
17
- return `${prefix}/embed/instagram-asset?url=${encodeURIComponent(url.replace(AMP_RE, "&"))}`;
17
+ export function proxyAssetUrl(url, prefix = "/_scripts", secret) {
18
+ return buildProxyUrl(`${prefix}/embed/instagram-asset`, { url: url.replace(AMP_RE, "&") }, secret);
18
19
  }
19
- export function rewriteUrl(url, prefix = "/_scripts") {
20
+ export function rewriteUrl(url, prefix = "/_scripts", secret) {
20
21
  try {
21
22
  const parsed = new URL(url);
22
23
  if (parsed.hostname === INSTAGRAM_ASSET_HOST)
23
- return proxyAssetUrl(url, prefix);
24
+ return proxyAssetUrl(url, prefix, secret);
24
25
  if (INSTAGRAM_IMAGE_HOSTS.some((h) => parsed.hostname === h || parsed.hostname.endsWith(`.cdninstagram.com`)))
25
- return proxyImageUrl(url, prefix);
26
+ return proxyImageUrl(url, prefix, secret);
26
27
  } catch {
27
28
  }
28
29
  return url;
29
30
  }
30
- export function rewriteUrlsInText(text, prefix = "/_scripts") {
31
- return text.replace(SCONTENT_RE, (m) => proxyImageUrl(m, prefix)).replace(STATIC_CDN_RE, (m) => proxyAssetUrl(m, prefix)).replace(LOOKASIDE_RE, (m) => proxyImageUrl(m, prefix));
31
+ export function rewriteUrlsInText(text, prefix = "/_scripts", secret) {
32
+ return text.replace(SCONTENT_RE, (m) => proxyImageUrl(m, prefix, secret)).replace(STATIC_CDN_RE, (m) => proxyAssetUrl(m, prefix, secret)).replace(LOOKASIDE_RE, (m) => proxyImageUrl(m, prefix, secret));
32
33
  }
33
34
  export function scopeCss(css, scopeSelector) {
34
35
  let result = css.replace(CHARSET_RE, "");
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Build a proxy URL with query params, signing it when a secret is available.
3
+ *
4
+ * Used by embed handlers that inject proxy URLs into HTML/JSON responses.
5
+ * When `secret` is set, URLs are HMAC-signed so clients can fetch them without
6
+ * needing a page token. When it's undefined, URLs fall back to unsigned form
7
+ * (which is only safe when the `withSigning` middleware has no secret either).
8
+ */
9
+ export declare function buildProxyUrl(path: string, query: Record<string, unknown>, secret?: string): string;
@@ -0,0 +1,21 @@
1
+ import { buildSignedProxyUrl } from "./sign.js";
2
+ export function buildProxyUrl(path, query, secret) {
3
+ if (secret)
4
+ return buildSignedProxyUrl(path, query, secret);
5
+ const parts = [];
6
+ for (const [key, value] of Object.entries(query)) {
7
+ if (value === void 0 || value === null)
8
+ continue;
9
+ const encodedKey = encodeURIComponent(key);
10
+ if (Array.isArray(value)) {
11
+ for (const item of value) {
12
+ if (item === void 0 || item === null)
13
+ continue;
14
+ parts.push(`${encodedKey}=${encodeURIComponent(String(item))}`);
15
+ }
16
+ } else {
17
+ parts.push(`${encodedKey}=${encodeURIComponent(String(value))}`);
18
+ }
19
+ }
20
+ return parts.length ? `${path}?${parts.join("&")}` : path;
21
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Signing constants shared between server (HMAC) and client (page-token) code.
3
+ *
4
+ * Kept in a crypto-free module so client bundles can import the param names
5
+ * without pulling in `node:crypto`.
6
+ */
7
+ /** Query param name for the HMAC signature. */
8
+ export declare const SIG_PARAM = "sig";
9
+ /** Length of the hex signature (16 chars = 64 bits). */
10
+ export declare const SIG_LENGTH = 16;
11
+ /** Query param name for the page token. */
12
+ export declare const PAGE_TOKEN_PARAM = "_pt";
13
+ /** Query param name for the page token timestamp. */
14
+ export declare const PAGE_TOKEN_TS_PARAM = "_ts";
15
+ /** Default max age for page tokens in seconds (1 hour). */
16
+ export declare const PAGE_TOKEN_MAX_AGE = 3600;
@@ -0,0 +1,5 @@
1
+ export const SIG_PARAM = "sig";
2
+ export const SIG_LENGTH = 16;
3
+ export const PAGE_TOKEN_PARAM = "_pt";
4
+ export const PAGE_TOKEN_TS_PARAM = "_ts";
5
+ export const PAGE_TOKEN_MAX_AGE = 3600;
@@ -22,10 +22,8 @@
22
22
  * prerendered HTML for no practical gain.
23
23
  */
24
24
  import type { H3Event } from 'h3';
25
- /** Query param name for the signature. Chosen to be unlikely to collide with upstream APIs. */
26
- export declare const SIG_PARAM = "sig";
27
- /** Length of the hex signature (16 chars = 64 bits). */
28
- export declare const SIG_LENGTH = 16;
25
+ import { PAGE_TOKEN_MAX_AGE, PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, SIG_LENGTH, SIG_PARAM } from './sign-constants.js';
26
+ export { PAGE_TOKEN_MAX_AGE, PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, SIG_LENGTH, SIG_PARAM };
29
27
  /**
30
28
  * Canonicalize a query object into a deterministic string suitable for HMAC input.
31
29
  *
@@ -61,12 +59,6 @@ export declare function signProxyUrl(path: string, query: Record<string, unknown
61
59
  * (SSR components, server-side URL rewriters like instagram-embed).
62
60
  */
63
61
  export declare function buildSignedProxyUrl(path: string, query: Record<string, unknown>, secret: string): string;
64
- /** Query param name for the page token. */
65
- export declare const PAGE_TOKEN_PARAM = "_pt";
66
- /** Query param name for the page token timestamp. */
67
- export declare const PAGE_TOKEN_TS_PARAM = "_ts";
68
- /** Default max age for page tokens in seconds (1 hour). */
69
- export declare const PAGE_TOKEN_MAX_AGE = 3600;
70
62
  /**
71
63
  * Generate a page token that authorizes client-side proxy requests.
72
64
  *
@@ -1,7 +1,13 @@
1
1
  import { createHmac } from "node:crypto";
2
2
  import { getQuery } from "h3";
3
- export const SIG_PARAM = "sig";
4
- export const SIG_LENGTH = 16;
3
+ import {
4
+ PAGE_TOKEN_MAX_AGE,
5
+ PAGE_TOKEN_PARAM,
6
+ PAGE_TOKEN_TS_PARAM,
7
+ SIG_LENGTH,
8
+ SIG_PARAM
9
+ } from "./sign-constants.js";
10
+ export { PAGE_TOKEN_MAX_AGE, PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, SIG_LENGTH, SIG_PARAM };
5
11
  export function canonicalizeQuery(query) {
6
12
  const keys = Object.keys(query).filter((k) => k !== SIG_PARAM && query[k] !== void 0 && query[k] !== null).sort();
7
13
  const parts = [];
@@ -38,9 +44,6 @@ export function buildSignedProxyUrl(path, query, secret) {
38
44
  const queryString = canonical ? `${canonical}&${SIG_PARAM}=${sig}` : `${SIG_PARAM}=${sig}`;
39
45
  return `${path}?${queryString}`;
40
46
  }
41
- export const PAGE_TOKEN_PARAM = "_pt";
42
- export const PAGE_TOKEN_TS_PARAM = "_ts";
43
- export const PAGE_TOKEN_MAX_AGE = 3600;
44
47
  export function generateProxyToken(secret, timestamp) {
45
48
  return createHmac("sha256", secret).update(`proxy-access:${timestamp}`).digest("hex").slice(0, SIG_LENGTH);
46
49
  }
@@ -4,10 +4,11 @@ import { verifyProxyRequest } from "./sign.js";
4
4
  export function withSigning(handler) {
5
5
  return defineEventHandler(async (event) => {
6
6
  const runtimeConfig = useRuntimeConfig(event);
7
- const secret = runtimeConfig["nuxt-scripts"]?.proxySecret;
7
+ const scriptsConfig = runtimeConfig["nuxt-scripts"];
8
+ const secret = scriptsConfig?.proxySecret;
8
9
  if (!secret)
9
10
  return handler(event);
10
- if (!verifyProxyRequest(event, secret)) {
11
+ if (!verifyProxyRequest(event, secret, scriptsConfig?.pageTokenMaxAge)) {
11
12
  throw createError({
12
13
  statusCode: 403,
13
14
  statusMessage: "Invalid signature"
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Buffer<ArrayBufferLike>>>;
2
2
  export default _default;
@@ -1,7 +1,19 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
- import { $fetch } from "ofetch";
2
+ import { useRuntimeConfig } from "nitropack/runtime";
3
+ import { createCachedJsonFetch } from "./utils/cached-upstream.js";
4
+ import { rewriteTweetImages } from "./utils/embed-rewriters.js";
3
5
  import { withSigning } from "./utils/withSigning.js";
4
6
  const TWEET_ID_RE = /^\d+$/;
7
+ const EMBED_X_SUFFIX_RE = /\/embed\/x$/;
8
+ const TWEET_ID_FROM_URL_RE = /[?&]id=(\d+)/;
9
+ const cachedTweetFetch = createCachedJsonFetch(
10
+ "nuxt-scripts-x-tweet",
11
+ 600,
12
+ (url) => {
13
+ const match = url.match(TWEET_ID_FROM_URL_RE);
14
+ return match?.[1] || url;
15
+ }
16
+ );
5
17
  export default withSigning(defineEventHandler(async (event) => {
6
18
  const query = getQuery(event);
7
19
  const tweetId = query.id;
@@ -13,7 +25,7 @@ export default withSigning(defineEventHandler(async (event) => {
13
25
  }
14
26
  const randomToken = Array.from(Array.from({ length: 11 }), () => (Math.random() * 36).toString(36)[2]).join("");
15
27
  const params = new URLSearchParams({ id: tweetId, token: randomToken });
16
- const tweetData = await $fetch(
28
+ const tweetRaw = await cachedTweetFetch(
17
29
  `https://cdn.syndication.twimg.com/tweet-result?${params.toString()}`,
18
30
  {
19
31
  headers: {
@@ -27,6 +39,12 @@ export default withSigning(defineEventHandler(async (event) => {
27
39
  statusMessage: error.statusMessage || "Failed to fetch tweet"
28
40
  });
29
41
  });
42
+ const tweetData = structuredClone(tweetRaw);
43
+ const handlerPath = event.path?.split("?")[0] || "";
44
+ const prefix = handlerPath.replace(EMBED_X_SUFFIX_RE, "") || "/_scripts";
45
+ const imagePath = `${prefix}/embed/x-image`;
46
+ const secret = useRuntimeConfig(event)["nuxt-scripts"]?.proxySecret;
47
+ rewriteTweetImages(tweetData, imagePath, secret);
30
48
  setHeader(event, "Content-Type", "application/json");
31
49
  setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
32
50
  return tweetData;
@@ -44,12 +44,35 @@ import type { ProxyPrivacyInput } from './server/utils/privacy.js';
44
44
  export type { Cluster, ClusterStats, MarkerClustererContext, MarkerClustererInstance, MarkerClustererOptions } from './components/GoogleMaps/types.js';
45
45
  export { MARKER_CLUSTERER_INJECTION_KEY } from './components/GoogleMaps/types.js';
46
46
  export type WarmupStrategy = false | 'preload' | 'preconnect' | 'dns-prefetch';
47
- export type UseScriptContext<T extends Record<symbol | string, any>> = VueScriptInstance<T> & {
47
+ /**
48
+ * GCMv2 consent category value.
49
+ * @see https://developers.google.com/tag-platform/security/guides/consent
50
+ */
51
+ export type ConsentCategoryValue = 'granted' | 'denied';
52
+ /**
53
+ * Canonical GCMv2 consent state shape used by vendors that natively consume
54
+ * Consent Mode v2 (Google Analytics, Google Tag Manager, Bing UET).
55
+ */
56
+ export interface ConsentState {
57
+ ad_storage?: ConsentCategoryValue;
58
+ ad_user_data?: ConsentCategoryValue;
59
+ ad_personalization?: ConsentCategoryValue;
60
+ analytics_storage?: ConsentCategoryValue;
61
+ functionality_storage?: ConsentCategoryValue;
62
+ personalization_storage?: ConsentCategoryValue;
63
+ security_storage?: ConsentCategoryValue;
64
+ }
65
+ export type UseScriptContext<T extends Record<symbol | string, any>, C = unknown> = VueScriptInstance<T> & {
48
66
  /**
49
67
  * Remove and reload the script. Useful for scripts that need to re-execute
50
68
  * after SPA navigation (e.g., DOM-scanning scripts like iubenda).
51
69
  */
52
70
  reload: () => Promise<T>;
71
+ /**
72
+ * Vendor-native consent controls attached by registry scripts.
73
+ * Shape depends on the vendor (GCMv2 update, binary grant/revoke, three-state, etc.).
74
+ */
75
+ consent?: C;
53
76
  };
54
77
  export type NuxtUseScriptOptions<T extends Record<symbol | string, any> = {}> = Omit<UseScriptOptions<T>, 'trigger'> & {
55
78
  /**