@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.
- package/dist/devtools-client/200.html +1 -1
- package/dist/devtools-client/404.html +1 -1
- package/dist/devtools-client/_nuxt/{BBS9G2Kb.js → Br5kvbNb.js} +1 -1
- package/dist/devtools-client/_nuxt/C25MBdR1.js +1 -0
- package/dist/devtools-client/_nuxt/{DCBsJT4N.js → Cg_OIb5q.js} +1 -1
- package/dist/devtools-client/_nuxt/{B4uHpJPz.js → D2o5loaz.js} +1 -1
- package/dist/devtools-client/_nuxt/De7Wf2b9.js +188 -0
- package/dist/devtools-client/_nuxt/{Cxq4HLPL.js → DnVCfhVR.js} +1 -1
- package/dist/devtools-client/_nuxt/builds/latest.json +1 -1
- package/dist/devtools-client/_nuxt/builds/meta/9d868e70-bc5a-425c-8c84-8defe5186920.json +1 -0
- package/dist/devtools-client/_nuxt/{entry.BwpOBArY.css → entry.BSxy0W1q.css} +1 -1
- package/dist/devtools-client/_nuxt/index.DZD1lwyI.css +1 -0
- package/dist/devtools-client/_nuxt/{DvZScWzI.js → pN4-T8ZD.js} +1 -1
- package/dist/devtools-client/docs/index.html +1 -1
- package/dist/devtools-client/first-party/index.html +1 -1
- package/dist/devtools-client/index.html +1 -1
- package/dist/devtools-client/registry/index.html +1 -1
- package/dist/module.d.mts +15 -0
- package/dist/module.d.ts +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +35 -8
- package/dist/registry.mjs +3 -3
- package/dist/runtime/components/GoogleMaps/ScriptGoogleMapsStaticMap.vue +6 -2
- package/dist/runtime/components/ScriptBlueskyEmbed.d.vue.ts +0 -1
- package/dist/runtime/components/ScriptBlueskyEmbed.vue +12 -10
- package/dist/runtime/components/ScriptBlueskyEmbed.vue.d.ts +0 -1
- package/dist/runtime/components/ScriptInstagramEmbed.vue +3 -1
- package/dist/runtime/components/ScriptXEmbed.d.vue.ts +0 -1
- package/dist/runtime/components/ScriptXEmbed.vue +11 -9
- package/dist/runtime/components/ScriptXEmbed.vue.d.ts +0 -1
- package/dist/runtime/composables/useScript.js +17 -6
- package/dist/runtime/composables/useScriptProxyToken.d.ts +12 -0
- package/dist/runtime/composables/useScriptProxyToken.js +4 -0
- package/dist/runtime/composables/useScriptProxyUrl.d.ts +12 -0
- package/dist/runtime/composables/useScriptProxyUrl.js +27 -0
- package/dist/runtime/plugins/proxy-token.server.d.ts +10 -0
- package/dist/runtime/plugins/proxy-token.server.js +17 -0
- package/dist/runtime/registry/bing-uet.d.ts +189 -11
- package/dist/runtime/registry/bing-uet.js +16 -2
- package/dist/runtime/registry/bluesky-embed.d.ts +0 -4
- package/dist/runtime/registry/bluesky-embed.js +0 -4
- package/dist/runtime/registry/clarity.d.ts +6 -2
- package/dist/runtime/registry/clarity.js +12 -1
- package/dist/runtime/registry/google-analytics.d.ts +6 -2
- package/dist/runtime/registry/google-analytics.js +12 -1
- package/dist/runtime/registry/google-tag-manager.d.ts +6 -2
- package/dist/runtime/registry/google-tag-manager.js +10 -1
- package/dist/runtime/registry/gravatar.js +10 -13
- package/dist/runtime/registry/matomo-analytics.d.ts +9 -3
- package/dist/runtime/registry/matomo-analytics.js +28 -1
- package/dist/runtime/registry/meta-pixel.d.ts +8 -2
- package/dist/runtime/registry/meta-pixel.js +10 -1
- package/dist/runtime/registry/mixpanel-analytics.d.ts +12 -2
- package/dist/runtime/registry/mixpanel-analytics.js +16 -4
- package/dist/runtime/registry/posthog.d.ts +8 -2
- package/dist/runtime/registry/posthog.js +15 -4
- package/dist/runtime/registry/schemas.d.ts +65 -0
- package/dist/runtime/registry/schemas.js +75 -8
- package/dist/runtime/registry/tiktok-pixel.d.ts +16 -2
- package/dist/runtime/registry/tiktok-pixel.js +22 -1
- package/dist/runtime/registry/x-embed.d.ts +0 -4
- package/dist/runtime/registry/x-embed.js +0 -4
- package/dist/runtime/server/bluesky-embed-image.d.ts +1 -1
- package/dist/runtime/server/bluesky-embed.d.ts +1 -15
- package/dist/runtime/server/bluesky-embed.js +22 -4
- package/dist/runtime/server/google-maps-geocode-proxy.js +8 -5
- package/dist/runtime/server/google-static-maps-proxy.d.ts +1 -1
- package/dist/runtime/server/google-static-maps-proxy.js +13 -8
- package/dist/runtime/server/gravatar-proxy.d.ts +1 -1
- package/dist/runtime/server/gravatar-proxy.js +6 -7
- package/dist/runtime/server/instagram-embed-asset.d.ts +1 -1
- package/dist/runtime/server/instagram-embed-image.d.ts +1 -1
- package/dist/runtime/server/instagram-embed.js +22 -10
- package/dist/runtime/server/utils/cached-upstream.d.ts +55 -0
- package/dist/runtime/server/utils/cached-upstream.js +65 -0
- package/dist/runtime/server/utils/embed-rewriters.d.ts +19 -0
- package/dist/runtime/server/utils/embed-rewriters.js +41 -0
- package/dist/runtime/server/utils/image-proxy.d.ts +3 -1
- package/dist/runtime/server/utils/image-proxy.js +8 -6
- package/dist/runtime/server/utils/instagram-embed.d.ts +4 -4
- package/dist/runtime/server/utils/instagram-embed.js +10 -9
- package/dist/runtime/server/utils/proxy-url.d.ts +9 -0
- package/dist/runtime/server/utils/proxy-url.js +21 -0
- package/dist/runtime/server/utils/sign-constants.d.ts +16 -0
- package/dist/runtime/server/utils/sign-constants.js +5 -0
- package/dist/runtime/server/utils/sign.d.ts +2 -10
- package/dist/runtime/server/utils/sign.js +8 -5
- package/dist/runtime/server/utils/withSigning.js +3 -2
- package/dist/runtime/server/x-embed-image.d.ts +1 -1
- package/dist/runtime/server/x-embed.js +20 -2
- package/dist/runtime/types.d.ts +24 -1
- package/dist/types-source.mjs +160 -12
- package/package.json +2 -2
- package/dist/devtools-client/_nuxt/CQR4zIAm.js +0 -1
- package/dist/devtools-client/_nuxt/DTxy5P8N.js +0 -188
- package/dist/devtools-client/_nuxt/builds/meta/484f72b9-a019-4127-8ab9-c10e92624094.json +0 -1
- 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 {
|
|
2
|
+
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from "ultrahtml";
|
|
4
|
-
import {
|
|
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
|
|
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) =>
|
|
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(${
|
|
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 & 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<
|
|
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 {
|
|
2
|
+
import { createCachedBinaryFetch } from "./cached-upstream.js";
|
|
3
3
|
import { withSigning } from "./withSigning.js";
|
|
4
4
|
const AMP_RE = /&/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
|
|
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 &&
|
|
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",
|
|
69
|
+
setHeader(event, "Content-Type", result.contentType || contentType);
|
|
68
70
|
setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
|
|
69
|
-
return
|
|
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 = /&/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
|
|
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
|
|
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;
|
|
@@ -22,10 +22,8 @@
|
|
|
22
22
|
* prerendered HTML for no practical gain.
|
|
23
23
|
*/
|
|
24
24
|
import type { H3Event } from 'h3';
|
|
25
|
-
|
|
26
|
-
export
|
|
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
|
-
|
|
4
|
-
|
|
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
|
|
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<
|
|
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 {
|
|
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
|
|
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;
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|