@nuxt/scripts 1.3.3 → 1.3.4
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/{DAubxma0.js → 8ltZ0o6d.js} +1 -1
- package/dist/devtools-client/_nuxt/{CBuBrHyL.js → B8_EBdhg.js} +1 -1
- package/dist/devtools-client/_nuxt/CC0GKROJ.js +1 -0
- package/dist/devtools-client/_nuxt/{DvC3yV2P.js → CMXzQxJZ.js} +1 -1
- package/dist/devtools-client/_nuxt/{xAZ2vgo3.js → CP7c-Js6.js} +4 -4
- package/dist/devtools-client/_nuxt/{DzbSCA_w.js → DTP8MRtW.js} +1 -1
- package/dist/devtools-client/_nuxt/{DgAtAWdT.js → Dkc7iTdR.js} +1 -1
- package/dist/devtools-client/_nuxt/Dl7W1ViV.js +28 -0
- package/dist/devtools-client/_nuxt/{WOtyQdps.js → KxC4Q3ev.js} +1 -1
- package/dist/devtools-client/_nuxt/builds/latest.json +1 -1
- package/dist/devtools-client/_nuxt/builds/meta/c38edf4f-387c-4148-a34a-ca78a5b40869.json +1 -0
- package/dist/devtools-client/_nuxt/error-404.Bq2JQbJs.css +1 -0
- package/dist/devtools-client/_nuxt/error-500.5E_FOzlc.css +1 -0
- package/dist/devtools-client/_nuxt/{pages.BF1VnccH.css → pages.aU4WirOM.css} +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 +1 -1
- package/dist/module.d.ts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +263 -47
- package/dist/registry.mjs +2 -2
- package/dist/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +71 -21
- package/dist/runtime/components/GoogleMaps/useGoogleMapsResource.d.ts +2 -1
- package/dist/runtime/components/GoogleMaps/useGoogleMapsResource.js +39 -25
- package/dist/runtime/components/ScriptCarbonAds.vue +11 -3
- package/dist/runtime/components/ScriptCrisp.vue +4 -1
- package/dist/runtime/components/ScriptIntercom.vue +4 -1
- package/dist/runtime/components/ScriptLemonSqueezy.d.vue.ts +13 -13
- package/dist/runtime/components/ScriptLemonSqueezy.vue +30 -6
- package/dist/runtime/components/ScriptLemonSqueezy.vue.d.ts +13 -13
- package/dist/runtime/components/ScriptPayPalButtons.vue +8 -1
- package/dist/runtime/components/ScriptPayPalMessages.vue +6 -0
- package/dist/runtime/components/ScriptStripePricingTable.vue +2 -0
- package/dist/runtime/components/ScriptVimeoPlayer.vue +8 -1
- package/dist/runtime/composables/useScript.js +121 -44
- package/dist/runtime/composables/useScriptEventPage.js +36 -20
- package/dist/runtime/composables/useScriptTriggerIdleTimeout.js +19 -5
- package/dist/runtime/composables/useScriptTriggerInteraction.js +23 -10
- package/dist/runtime/composables/useScriptTriggerServiceWorker.js +21 -18
- package/dist/runtime/devtools-standalone-bridge.client.js +28 -3
- package/dist/runtime/npm-script-proxy.d.ts +7 -0
- package/dist/runtime/npm-script-proxy.js +46 -0
- package/dist/runtime/npm-script-stub.js +4 -5
- package/dist/runtime/registry/posthog.js +11 -3
- package/dist/runtime/registry/speedcurve.js +33 -4
- package/dist/runtime/registry/usercentrics.js +4 -21
- package/dist/runtime/registry/youtube-player.js +16 -5
- package/dist/runtime/server/bluesky-embed.js +15 -6
- package/dist/runtime/server/google-maps-geocode-proxy.js +10 -5
- package/dist/runtime/server/google-static-maps-proxy.js +20 -13
- package/dist/runtime/server/gravatar-proxy.js +48 -11
- package/dist/runtime/server/instagram-embed-asset.js +1 -1
- package/dist/runtime/server/instagram-embed.js +44 -94
- package/dist/runtime/server/proxy-handler.d.ts +2 -1
- package/dist/runtime/server/proxy-handler.js +183 -23
- package/dist/runtime/server/utils/cache-config.d.ts +14 -0
- package/dist/runtime/server/utils/cache-config.js +13 -0
- package/dist/runtime/server/utils/cached-upstream.d.ts +23 -2
- package/dist/runtime/server/utils/cached-upstream.js +212 -15
- package/dist/runtime/server/utils/image-proxy.d.ts +3 -2
- package/dist/runtime/server/utils/image-proxy.js +26 -16
- package/dist/runtime/server/utils/instagram-embed.d.ts +8 -0
- package/dist/runtime/server/utils/instagram-embed.js +155 -0
- package/dist/runtime/server/utils/network-host.d.ts +21 -0
- package/dist/runtime/server/utils/network-host.js +112 -0
- package/dist/runtime/server/utils/privacy.js +55 -19
- package/dist/runtime/server/utils/proxy-query.d.ts +2 -0
- package/dist/runtime/server/utils/proxy-query.js +10 -0
- package/dist/runtime/server/utils/sign.d.ts +1 -1
- package/dist/runtime/server/utils/sign.js +1 -1
- package/dist/runtime/server/utils/withSigning.d.ts +1 -1
- package/dist/runtime/server/utils/withSigning.js +3 -3
- package/dist/runtime/server/vercel-insights-sink.js +1 -1
- package/dist/runtime/server/x-embed.js +8 -4
- package/dist/runtime/unhead-features.d.ts +1 -0
- package/dist/runtime/unhead-features.js +3 -0
- package/dist/runtime/utils/abortable-promise.d.ts +8 -0
- package/dist/runtime/utils/abortable-promise.js +63 -0
- package/dist/runtime/utils/after-next-paint.d.ts +1 -1
- package/dist/runtime/utils/after-next-paint.js +16 -1
- package/dist/runtime/utils/usercentrics-consent.d.ts +10 -0
- package/dist/runtime/utils/usercentrics-consent.js +88 -0
- package/dist/runtime/utils/youtube-readiness.d.ts +7 -0
- package/dist/runtime/utils/youtube-readiness.js +45 -0
- package/dist/runtime/utils.d.ts +3 -1
- package/dist/runtime/utils.js +29 -0
- package/package.json +2 -1
- package/dist/devtools-client/_nuxt/BGGk5RYW.js +0 -1
- package/dist/devtools-client/_nuxt/C8xLTMBy.js +0 -28
- package/dist/devtools-client/_nuxt/builds/meta/0c193d7f-fdc4-4ec6-b4c0-7ba4cc94709b.json +0 -1
- package/dist/devtools-client/_nuxt/error-404.CIn5oEzb.css +0 -1
- package/dist/devtools-client/_nuxt/error-500.B1YZl8g8.css +0 -1
|
@@ -31,7 +31,12 @@ export function useScriptPostHog(_options) {
|
|
|
31
31
|
return { posthog };
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
clientInit: import.meta.server ? void 0 : async () => {
|
|
34
|
+
clientInit: import.meta.server ? void 0 : async (ctx) => {
|
|
35
|
+
const throwIfAborted = () => {
|
|
36
|
+
if (ctx?.signal.aborted)
|
|
37
|
+
throw ctx.signal.reason || new Error("Loading PostHog was aborted");
|
|
38
|
+
};
|
|
39
|
+
throwIfAborted();
|
|
35
40
|
if (window.__posthogInitPromise || window.posthog)
|
|
36
41
|
return;
|
|
37
42
|
if (!options?.apiKey) {
|
|
@@ -43,6 +48,7 @@ export function useScriptPostHog(_options) {
|
|
|
43
48
|
if (apiHost.startsWith("/"))
|
|
44
49
|
apiHost = window.location.origin + apiHost;
|
|
45
50
|
window.__posthogInitPromise = import("posthog-js").then(({ default: posthog }) => {
|
|
51
|
+
throwIfAborted();
|
|
46
52
|
const config = {
|
|
47
53
|
api_host: apiHost,
|
|
48
54
|
...options?.config
|
|
@@ -72,9 +78,11 @@ export function useScriptPostHog(_options) {
|
|
|
72
78
|
}
|
|
73
79
|
return window.posthog;
|
|
74
80
|
}).catch((e) => {
|
|
75
|
-
|
|
81
|
+
if (!ctx?.signal.aborted)
|
|
82
|
+
logger.error("Failed to load posthog-js:", e);
|
|
76
83
|
delete window._posthogQueue;
|
|
77
|
-
|
|
84
|
+
delete window.__posthogInitPromise;
|
|
85
|
+
throw e;
|
|
78
86
|
});
|
|
79
87
|
return window.__posthogInitPromise;
|
|
80
88
|
}
|
|
@@ -8,6 +8,8 @@ const LUX_USER_CONFIG_KEYS = Object.keys(SpeedCurveOptions.entries).filter(
|
|
|
8
8
|
(k) => k !== "id" && k !== "autoTrackSpaNavigations" && k !== "spaMode"
|
|
9
9
|
);
|
|
10
10
|
let luxWired = false;
|
|
11
|
+
let teardownAutoTracker = () => {
|
|
12
|
+
};
|
|
11
13
|
export function useScriptSpeedCurve(_options) {
|
|
12
14
|
return useRegistryScript("speedcurve", (options) => ({
|
|
13
15
|
scriptInput: {
|
|
@@ -60,7 +62,7 @@ export function installAutoTracker(options) {
|
|
|
60
62
|
const nuxt = useNuxtApp();
|
|
61
63
|
let pendingInitial = nuxt.payload?.serverRendered === false;
|
|
62
64
|
const label = options?.label === false ? null : typeof options?.label === "function" ? options.label : (to) => String(to.name ?? to.path);
|
|
63
|
-
router.beforeEach((to) => {
|
|
65
|
+
const stopBeforeEach = router.beforeEach((to) => {
|
|
64
66
|
const lux = window.LUX;
|
|
65
67
|
if (!lux)
|
|
66
68
|
return;
|
|
@@ -80,7 +82,7 @@ export function installAutoTracker(options) {
|
|
|
80
82
|
lux.label = nextLabel;
|
|
81
83
|
}
|
|
82
84
|
});
|
|
83
|
-
router.afterEach((_to, _from, failure) => {
|
|
85
|
+
const stopAfterEach = router.afterEach((_to, _from, failure) => {
|
|
84
86
|
if (!failure)
|
|
85
87
|
return;
|
|
86
88
|
const lux = window.LUX;
|
|
@@ -89,7 +91,34 @@ export function installAutoTracker(options) {
|
|
|
89
91
|
lux.markLoadTime();
|
|
90
92
|
lux.addData("luxNavFailed", "1");
|
|
91
93
|
});
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
const pendingPaints = /* @__PURE__ */ new Set();
|
|
95
|
+
const stopPageFinish = nuxt.hook("page:finish", () => {
|
|
96
|
+
const pending = { cancel: () => {
|
|
97
|
+
} };
|
|
98
|
+
pendingPaints.add(pending);
|
|
99
|
+
pending.cancel = afterNextPaint(() => {
|
|
100
|
+
pendingPaints.delete(pending);
|
|
101
|
+
window.LUX?.markLoadTime();
|
|
102
|
+
});
|
|
94
103
|
});
|
|
104
|
+
let stopAppUnmount = () => {
|
|
105
|
+
};
|
|
106
|
+
const cleanup = () => {
|
|
107
|
+
if (!luxWired)
|
|
108
|
+
return;
|
|
109
|
+
luxWired = false;
|
|
110
|
+
stopBeforeEach();
|
|
111
|
+
stopAfterEach();
|
|
112
|
+
stopPageFinish();
|
|
113
|
+
stopAppUnmount();
|
|
114
|
+
for (const pending of pendingPaints)
|
|
115
|
+
pending.cancel();
|
|
116
|
+
pendingPaints.clear();
|
|
117
|
+
teardownAutoTracker = () => {
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
stopAppUnmount = nuxt.hook("app:unmount", cleanup);
|
|
121
|
+
teardownAutoTracker = cleanup;
|
|
95
122
|
}
|
|
123
|
+
if (import.meta.hot)
|
|
124
|
+
import.meta.hot.dispose(() => teardownAutoTracker());
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useHead } from "@unhead/vue";
|
|
2
|
+
import { useNuxtApp } from "nuxt/app";
|
|
2
3
|
import { useRegistryScript } from "../utils.js";
|
|
4
|
+
import { attachUsercentricsConsent } from "../utils/usercentrics-consent.js";
|
|
3
5
|
import { UsercentricsOptions } from "./schemas.js";
|
|
4
6
|
export { UsercentricsOptions };
|
|
5
7
|
export function useScriptUsercentrics(_options) {
|
|
@@ -29,26 +31,7 @@ export function useScriptUsercentrics(_options) {
|
|
|
29
31
|
}
|
|
30
32
|
};
|
|
31
33
|
}, _options);
|
|
32
|
-
if (import.meta.client
|
|
33
|
-
|
|
34
|
-
const onReady = () => {
|
|
35
|
-
window.removeEventListener("UC_CMP_API_READY", onReady);
|
|
36
|
-
resolve(window.__ucCmp);
|
|
37
|
-
};
|
|
38
|
-
window.addEventListener("UC_CMP_API_READY", onReady);
|
|
39
|
-
});
|
|
40
|
-
instance.consent = {
|
|
41
|
-
whenReady,
|
|
42
|
-
onConsentChange(cb) {
|
|
43
|
-
const handler = (e) => cb(e.detail, e);
|
|
44
|
-
window.addEventListener("UC_UI_CMP_EVENT", handler);
|
|
45
|
-
return () => window.removeEventListener("UC_UI_CMP_EVENT", handler);
|
|
46
|
-
},
|
|
47
|
-
showFirstLayer: () => window.__ucCmp?.showFirstLayer?.(),
|
|
48
|
-
showSecondLayer: () => window.__ucCmp?.showSecondLayer?.(),
|
|
49
|
-
acceptAll: () => window.__ucCmp?.acceptAllConsents?.(),
|
|
50
|
-
denyAll: () => window.__ucCmp?.denyAllConsents?.()
|
|
51
|
-
};
|
|
52
|
-
}
|
|
34
|
+
if (import.meta.client)
|
|
35
|
+
attachUsercentricsConsent(instance, window, (callback) => useNuxtApp().hooks.hook("app:unmount", callback));
|
|
53
36
|
return instance;
|
|
54
37
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useHead } from "@unhead/vue";
|
|
2
2
|
import { watch } from "vue";
|
|
3
3
|
import { useRegistryScript } from "../utils.js";
|
|
4
|
+
import { armYouTubeReadiness, useYouTubeReadinessState } from "../utils/youtube-readiness.js";
|
|
5
|
+
const cleanupDecoration = Symbol("nuxt-scripts:youtube-cleanup");
|
|
6
|
+
const readinessDecoration = Symbol("nuxt-scripts:youtube-readiness");
|
|
4
7
|
export function useScriptYouTubePlayer(_options) {
|
|
5
|
-
let readyPromise = Promise.resolve();
|
|
6
8
|
const instance = useRegistryScript("youtubePlayer", () => ({
|
|
7
9
|
scriptInput: {
|
|
8
10
|
src: "https://www.youtube.com/iframe_api",
|
|
@@ -11,19 +13,28 @@ export function useScriptYouTubePlayer(_options) {
|
|
|
11
13
|
},
|
|
12
14
|
scriptOptions: {
|
|
13
15
|
use() {
|
|
16
|
+
const readiness = useYouTubeReadinessState();
|
|
14
17
|
return {
|
|
15
|
-
YT: window.YT ||
|
|
18
|
+
YT: window.YT || readiness.promise.then(() => {
|
|
16
19
|
return window.YT;
|
|
17
20
|
})
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
},
|
|
21
24
|
clientInit: import.meta.server ? void 0 : () => {
|
|
22
|
-
|
|
23
|
-
window.onYouTubeIframeAPIReady = resolve;
|
|
24
|
-
});
|
|
25
|
+
armYouTubeReadiness(useYouTubeReadinessState());
|
|
25
26
|
}
|
|
26
27
|
}), _options);
|
|
28
|
+
const clientInstance = import.meta.server || typeof window === "undefined" ? void 0 : instance;
|
|
29
|
+
if (clientInstance && !clientInstance[cleanupDecoration]) {
|
|
30
|
+
clientInstance[cleanupDecoration] = true;
|
|
31
|
+
clientInstance[readinessDecoration] = useYouTubeReadinessState();
|
|
32
|
+
const originalRemove = instance.remove;
|
|
33
|
+
instance.remove = () => {
|
|
34
|
+
clientInstance[readinessDecoration]?.controller?.abort();
|
|
35
|
+
return originalRemove();
|
|
36
|
+
};
|
|
37
|
+
}
|
|
27
38
|
if (import.meta.client) {
|
|
28
39
|
const _ = watch(instance.status, (status) => {
|
|
29
40
|
if (status === "loading") {
|
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
|
-
import { useRuntimeConfig } from "
|
|
3
|
-
import { createCachedJsonFetch } from "./utils/cached-upstream.js";
|
|
1
|
+
import { createError, defineEventHandler, getQuery, setHeader } from "#nuxt-scripts/h3";
|
|
2
|
+
import { useRuntimeConfig } from "#nuxt-scripts/nitro";
|
|
3
|
+
import { createCachedJsonFetch, isSafeHttpsUrl } from "./utils/cached-upstream.js";
|
|
4
4
|
import { rewriteBlueskyPostImages } from "./utils/embed-rewriters.js";
|
|
5
5
|
import { withSigning } from "./utils/withSigning.js";
|
|
6
6
|
const BSKY_POST_URL_RE = /^https:\/\/bsky\.app\/profile\/([^/]+)\/post\/([^/?]+)$/;
|
|
7
7
|
const EMBED_BSKY_SUFFIX_RE = /\/embed\/bluesky$/;
|
|
8
|
+
const allowBlueskyApiUrl = (url) => isSafeHttpsUrl(url) && url.hostname === "public.api.bsky.app";
|
|
8
9
|
const cachedProfileFetch = createCachedJsonFetch(
|
|
9
10
|
"nuxt-scripts-bsky-profile",
|
|
10
11
|
86400,
|
|
11
|
-
(url) => url
|
|
12
|
+
(url) => url,
|
|
13
|
+
{
|
|
14
|
+
allowUrl: allowBlueskyApiUrl,
|
|
15
|
+
contentTypePrefixes: ["application/json"]
|
|
16
|
+
}
|
|
12
17
|
);
|
|
13
18
|
const cachedPostFetch = createCachedJsonFetch(
|
|
14
19
|
"nuxt-scripts-bsky-post",
|
|
15
20
|
600,
|
|
16
|
-
(url) => url
|
|
21
|
+
(url) => url,
|
|
22
|
+
{
|
|
23
|
+
allowUrl: allowBlueskyApiUrl,
|
|
24
|
+
contentTypePrefixes: ["application/json"]
|
|
25
|
+
}
|
|
17
26
|
);
|
|
18
27
|
export default withSigning(defineEventHandler(async (event) => {
|
|
19
28
|
const query = getQuery(event);
|
|
@@ -70,7 +79,7 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
70
79
|
const handlerPath = event.path?.split("?")[0] || "";
|
|
71
80
|
const prefix = handlerPath.replace(EMBED_BSKY_SUFFIX_RE, "") || "/_scripts";
|
|
72
81
|
const imagePath = `${prefix}/embed/bluesky-image`;
|
|
73
|
-
const secret = useRuntimeConfig(
|
|
82
|
+
const secret = useRuntimeConfig()["nuxt-scripts"]?.proxySecret;
|
|
74
83
|
rewriteBlueskyPostImages(post, imagePath, secret);
|
|
75
84
|
setHeader(event, "Content-Type", "application/json");
|
|
76
85
|
setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
|
-
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
1
|
import { withQuery } from "ufo";
|
|
4
|
-
import {
|
|
2
|
+
import { createError, defineEventHandler, getQuery, setHeader } from "#nuxt-scripts/h3";
|
|
3
|
+
import { useRuntimeConfig } from "#nuxt-scripts/nitro";
|
|
4
|
+
import { createCachedJsonFetch, isSafeHttpsUrl } from "./utils/cached-upstream.js";
|
|
5
|
+
import { stripProxyAuthQuery } from "./utils/proxy-query.js";
|
|
5
6
|
import { withSigning } from "./utils/withSigning.js";
|
|
6
7
|
const cachedGeocodeFetch = createCachedJsonFetch(
|
|
7
8
|
"nuxt-scripts-geocode",
|
|
8
9
|
2592e3,
|
|
9
|
-
(url) => url
|
|
10
|
+
(url) => url,
|
|
11
|
+
{
|
|
12
|
+
allowUrl: (url) => isSafeHttpsUrl(url) && url.hostname === "maps.googleapis.com",
|
|
13
|
+
contentTypePrefixes: ["application/json"]
|
|
14
|
+
}
|
|
10
15
|
);
|
|
11
16
|
export default withSigning(defineEventHandler(async (event) => {
|
|
12
17
|
const runtimeConfig = useRuntimeConfig();
|
|
@@ -18,7 +23,7 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
18
23
|
statusMessage: "Google Maps API key not configured for geocode proxy"
|
|
19
24
|
});
|
|
20
25
|
}
|
|
21
|
-
const query = getQuery(event);
|
|
26
|
+
const query = stripProxyAuthQuery(getQuery(event));
|
|
22
27
|
const { key: _clientKey, ...safeQuery } = query;
|
|
23
28
|
const geocodeUrl = withQuery("https://maps.googleapis.com/maps/api/geocode/json", {
|
|
24
29
|
...safeQuery,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
|
-
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
1
|
import { withQuery } from "ufo";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { createError, defineEventHandler, getQuery, setHeader } from "#nuxt-scripts/h3";
|
|
3
|
+
import { useRuntimeConfig } from "#nuxt-scripts/nitro";
|
|
4
|
+
import { createCachedBinaryFetch, isSafeHttpsUrl } from "./utils/cached-upstream.js";
|
|
5
|
+
import { stripProxyAuthQuery } from "./utils/proxy-query.js";
|
|
6
6
|
import { withSigning } from "./utils/withSigning.js";
|
|
7
|
-
const cachedMapFetch = createCachedBinaryFetch("nuxt-scripts-static-map", 604800
|
|
8
|
-
|
|
7
|
+
const cachedMapFetch = createCachedBinaryFetch("nuxt-scripts-static-map", 604800, {
|
|
8
|
+
allowContentType: (contentType) => contentType.startsWith("image/") && contentType !== "image/svg+xml",
|
|
9
|
+
allowUrl: (url) => isSafeHttpsUrl(url) && url.hostname === "maps.googleapis.com"
|
|
10
|
+
});
|
|
9
11
|
export default withSigning(defineEventHandler(async (event) => {
|
|
10
12
|
const runtimeConfig = useRuntimeConfig();
|
|
11
13
|
const publicConfig = runtimeConfig.public["nuxt-scripts"]?.googleStaticMapsProxy;
|
|
@@ -23,12 +25,8 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
23
25
|
statusMessage: "Google Maps API key not configured for proxy"
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
|
-
const query = getQuery(event);
|
|
27
|
-
const safeQuery =
|
|
28
|
-
for (const [k, v] of Object.entries(query)) {
|
|
29
|
-
if (!STRIP_PARAMS.has(k))
|
|
30
|
-
safeQuery[k] = v;
|
|
31
|
-
}
|
|
28
|
+
const query = stripProxyAuthQuery(getQuery(event));
|
|
29
|
+
const { key: _clientKey, ...safeQuery } = query;
|
|
32
30
|
const googleMapsUrl = withQuery("https://maps.googleapis.com/maps/api/staticmap", {
|
|
33
31
|
...safeQuery,
|
|
34
32
|
key: apiKey
|
|
@@ -41,9 +39,18 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
41
39
|
statusMessage: error.statusMessage || "Failed to fetch static map"
|
|
42
40
|
});
|
|
43
41
|
});
|
|
42
|
+
const contentType = result.contentType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
43
|
+
if (!contentType?.startsWith("image/") || contentType === "image/svg+xml") {
|
|
44
|
+
throw createError({
|
|
45
|
+
statusCode: 415,
|
|
46
|
+
statusMessage: "Unsupported upstream content type"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
44
49
|
const cacheMaxAge = publicConfig.cacheMaxAge || 3600;
|
|
45
|
-
setHeader(event, "Content-Type", result.contentType
|
|
50
|
+
setHeader(event, "Content-Type", result.contentType);
|
|
46
51
|
setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
|
|
47
52
|
setHeader(event, "Vary", "Accept-Encoding");
|
|
53
|
+
setHeader(event, "Content-Security-Policy", "sandbox; default-src 'none'; base-uri 'none'; form-action 'none'");
|
|
54
|
+
setHeader(event, "X-Content-Type-Options", "nosniff");
|
|
48
55
|
return result.body;
|
|
49
56
|
}));
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
|
-
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
1
|
import { withQuery } from "ufo";
|
|
4
|
-
import {
|
|
2
|
+
import { createError, defineEventHandler, getQuery, setHeader } from "#nuxt-scripts/h3";
|
|
3
|
+
import { useRuntimeConfig } from "#nuxt-scripts/nitro";
|
|
4
|
+
import { createCachedBinaryFetch, isSafeHttpsUrl } from "./utils/cached-upstream.js";
|
|
5
5
|
import { withSigning } from "./utils/withSigning.js";
|
|
6
|
-
const
|
|
6
|
+
const GRAVATAR_HASH_RE = /^[a-f\d]{64}$/i;
|
|
7
|
+
const GRAVATAR_RATINGS = /* @__PURE__ */ new Set(["g", "pg", "r", "x"]);
|
|
8
|
+
function firstString(value) {
|
|
9
|
+
return Array.isArray(value) ? typeof value[0] === "string" ? value[0] : void 0 : typeof value === "string" ? value : void 0;
|
|
10
|
+
}
|
|
11
|
+
const cachedGravatarFetch = createCachedBinaryFetch("nuxt-scripts-gravatar", 3600, {
|
|
12
|
+
allowContentType: (contentType) => contentType.startsWith("image/") && contentType !== "image/svg+xml",
|
|
13
|
+
allowUrl: (url) => isSafeHttpsUrl(url) && (url.hostname === "gravatar.com" || url.hostname.endsWith(".gravatar.com"))
|
|
14
|
+
});
|
|
7
15
|
export default withSigning(defineEventHandler(async (event) => {
|
|
8
16
|
const runtimeConfig = useRuntimeConfig();
|
|
9
17
|
const proxyConfig = runtimeConfig.public["nuxt-scripts"]?.gravatarProxy;
|
|
10
18
|
const query = getQuery(event);
|
|
11
|
-
let hash = query.hash;
|
|
12
|
-
const email = query.email;
|
|
19
|
+
let hash = firstString(query.hash);
|
|
20
|
+
const email = firstString(query.email);
|
|
21
|
+
if (hash && !GRAVATAR_HASH_RE.test(hash)) {
|
|
22
|
+
throw createError({
|
|
23
|
+
statusCode: 400,
|
|
24
|
+
statusMessage: "Hash must be a 64-character SHA-256 hex digest"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
13
27
|
if (!hash && email) {
|
|
14
28
|
const encoder = new TextEncoder();
|
|
15
29
|
const data = encoder.encode(email.trim().toLowerCase());
|
|
@@ -22,11 +36,24 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
22
36
|
statusMessage: "Either hash or email parameter is required"
|
|
23
37
|
});
|
|
24
38
|
}
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
39
|
+
const rawSize = firstString(query.s) || "80";
|
|
40
|
+
const size = Number(rawSize);
|
|
41
|
+
const defaultImg = firstString(query.d) || "mp";
|
|
42
|
+
const rating = (firstString(query.r) || "g").toLowerCase();
|
|
43
|
+
if (!Number.isInteger(size) || size < 1 || size > 2048) {
|
|
44
|
+
throw createError({
|
|
45
|
+
statusCode: 400,
|
|
46
|
+
statusMessage: "Gravatar size must be an integer from 1 to 2048"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!GRAVATAR_RATINGS.has(rating)) {
|
|
50
|
+
throw createError({
|
|
51
|
+
statusCode: 400,
|
|
52
|
+
statusMessage: "Invalid Gravatar rating"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
28
55
|
const gravatarUrl = withQuery(`https://www.gravatar.com/avatar/${hash}`, {
|
|
29
|
-
s: size,
|
|
56
|
+
s: String(size),
|
|
30
57
|
d: defaultImg,
|
|
31
58
|
r: rating
|
|
32
59
|
});
|
|
@@ -39,8 +66,18 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
39
66
|
});
|
|
40
67
|
});
|
|
41
68
|
const cacheMaxAge = proxyConfig?.cacheMaxAge ?? 3600;
|
|
42
|
-
|
|
69
|
+
const responseContentType = result.contentType;
|
|
70
|
+
const upstreamContentType = responseContentType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
71
|
+
if (!responseContentType || !upstreamContentType?.startsWith("image/") || upstreamContentType === "image/svg+xml") {
|
|
72
|
+
throw createError({
|
|
73
|
+
statusCode: 415,
|
|
74
|
+
statusMessage: "Unsupported upstream content type"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
setHeader(event, "Content-Type", responseContentType);
|
|
43
78
|
setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
|
|
79
|
+
setHeader(event, "Content-Security-Policy", "sandbox; default-src 'none'");
|
|
44
80
|
setHeader(event, "Vary", "Accept-Encoding");
|
|
81
|
+
setHeader(event, "X-Content-Type-Options", "nosniff");
|
|
45
82
|
return result.body;
|
|
46
83
|
}));
|
|
@@ -4,6 +4,6 @@ export default createImageProxyHandler({
|
|
|
4
4
|
accept: "*/*",
|
|
5
5
|
userAgent: "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",
|
|
6
6
|
cacheMaxAge: 86400,
|
|
7
|
-
|
|
7
|
+
contentTypePrefixes: ["image/", "font/", "application/font", "application/x-font", "application/vnd.ms-fontobject", "application/octet-stream"],
|
|
8
8
|
decodeAmpersands: true
|
|
9
9
|
});
|
|
@@ -1,59 +1,47 @@
|
|
|
1
|
-
import { createError, defineEventHandler, getQuery, setHeader } from "h3";
|
|
2
|
-
import {
|
|
3
|
-
import { $fetch } from "ofetch";
|
|
4
|
-
import { hash } from "ohash";
|
|
5
|
-
import { ELEMENT_NODE, parse, renderSync, TEXT_NODE, walkSync } from "ultrahtml";
|
|
1
|
+
import { createError, defineEventHandler, getQuery, setHeader } from "#nuxt-scripts/h3";
|
|
2
|
+
import { useRuntimeConfig } from "#nuxt-scripts/nitro";
|
|
6
3
|
import { createCachedJsonFetch } from "./utils/cached-upstream.js";
|
|
7
|
-
import { isEmbedShell,
|
|
4
|
+
import { isEmbedShell, isSafeInstagramCssUrl, isSafeInstagramEmbedUrl, isSafeInstagramPostUrl, sanitizeInstagramEmbedCss, sanitizeInstagramEmbedHtml } from "./utils/instagram-embed.js";
|
|
8
5
|
import { withSigning } from "./utils/withSigning.js";
|
|
9
6
|
export { proxyAssetUrl, proxyImageUrl, rewriteUrl, rewriteUrlsInText, scopeCss } from "./utils/instagram-embed.js";
|
|
10
7
|
const EMBED_INSTAGRAM_SUFFIX_RE = /\/embed\/instagram$/;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
return html;
|
|
8
|
+
const cachedEmbedFetch = createCachedJsonFetch(
|
|
9
|
+
"nuxt-scripts-instagram-embed-v3",
|
|
10
|
+
600,
|
|
11
|
+
(url, opts) => {
|
|
12
|
+
const parts = [url];
|
|
13
|
+
for (const [key, value] of Object.entries(opts?.headers || {}).sort(([a], [b]) => a.localeCompare(b)))
|
|
14
|
+
parts.push(`${key}=${value}`);
|
|
15
|
+
return parts.join("\n");
|
|
22
16
|
},
|
|
23
17
|
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const parts = [url];
|
|
35
|
-
for (const [k, v] of Object.entries(headers).sort(([a], [b]) => a.localeCompare(b)))
|
|
36
|
-
parts.push(`${k}=${v}`);
|
|
37
|
-
return hash(parts);
|
|
18
|
+
allowUrl: isSafeInstagramEmbedUrl,
|
|
19
|
+
responseType: "text",
|
|
20
|
+
contentTypePrefixes: ["text/html"],
|
|
21
|
+
validateResponse: (html) => {
|
|
22
|
+
if (isEmbedShell(html)) {
|
|
23
|
+
throw createError({
|
|
24
|
+
statusCode: 502,
|
|
25
|
+
statusMessage: "Instagram returned an empty embed shell (post unavailable or upstream rate-limiting)"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
38
28
|
}
|
|
39
29
|
}
|
|
40
30
|
);
|
|
41
31
|
const cachedCssFetch = createCachedJsonFetch(
|
|
42
32
|
"nuxt-scripts-instagram-css",
|
|
43
33
|
86400,
|
|
44
|
-
(url) => url
|
|
34
|
+
(url) => url,
|
|
35
|
+
{
|
|
36
|
+
allowUrl: isSafeInstagramCssUrl,
|
|
37
|
+
responseType: "text",
|
|
38
|
+
contentTypePrefixes: ["text/css"]
|
|
39
|
+
}
|
|
45
40
|
);
|
|
46
|
-
function removeNode(node) {
|
|
47
|
-
node.type = TEXT_NODE;
|
|
48
|
-
node.value = "";
|
|
49
|
-
node.name = void 0;
|
|
50
|
-
node.attributes = {};
|
|
51
|
-
node.children = [];
|
|
52
|
-
}
|
|
53
41
|
export default withSigning(defineEventHandler(async (event) => {
|
|
54
42
|
const handlerPath = event.path?.split("?")[0] || "";
|
|
55
43
|
const prefix = handlerPath.replace(EMBED_INSTAGRAM_SUFFIX_RE, "") || "/_scripts";
|
|
56
|
-
const secret = useRuntimeConfig(
|
|
44
|
+
const secret = useRuntimeConfig()["nuxt-scripts"]?.proxySecret;
|
|
57
45
|
const query = getQuery(event);
|
|
58
46
|
const postUrl = query.url;
|
|
59
47
|
const captions = query.captions === "true";
|
|
@@ -72,7 +60,7 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
72
60
|
statusMessage: "Invalid postUrl"
|
|
73
61
|
});
|
|
74
62
|
}
|
|
75
|
-
if (!
|
|
63
|
+
if (!isSafeInstagramPostUrl(parsedUrl)) {
|
|
76
64
|
throw createError({
|
|
77
65
|
statusCode: 400,
|
|
78
66
|
statusMessage: "Invalid Instagram URL"
|
|
@@ -82,71 +70,31 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
82
70
|
const cleanUrl = parsedUrl.origin + pathname;
|
|
83
71
|
const embedUrl = `${cleanUrl}embed/${captions ? "captioned/" : ""}`;
|
|
84
72
|
const html = await cachedEmbedFetch(embedUrl, {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
headers: {
|
|
74
|
+
"Accept": "text/html",
|
|
75
|
+
// Meta's own crawler UA. Googlebot's UA is also accepted by Instagram
|
|
76
|
+
// but is IP-verified, so it fails from hosts outside Google's ranges
|
|
77
|
+
// (e.g. Cloudflare/Vercel) and Instagram serves the JS shell instead
|
|
78
|
+
// of the SSR'd post.
|
|
79
|
+
"User-Agent": "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)"
|
|
80
|
+
}
|
|
91
81
|
}).catch((error) => {
|
|
92
82
|
throw createError({
|
|
93
83
|
statusCode: error.statusCode || 500,
|
|
94
84
|
statusMessage: error.statusMessage || "Failed to fetch Instagram embed"
|
|
95
85
|
});
|
|
96
86
|
});
|
|
97
|
-
const
|
|
98
|
-
const cssUrls = [];
|
|
99
|
-
walkSync(ast, (node) => {
|
|
100
|
-
if (node.type !== ELEMENT_NODE)
|
|
101
|
-
return;
|
|
102
|
-
if (node.name === "link" && node.attributes.rel === "stylesheet" && node.attributes.href) {
|
|
103
|
-
cssUrls.push(node.attributes.href);
|
|
104
|
-
removeNode(node);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (node.name === "script" || node.name === "noscript" || node.name === "style") {
|
|
108
|
-
removeNode(node);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
for (const attr of ["src", "poster"]) {
|
|
112
|
-
if (node.attributes[attr])
|
|
113
|
-
node.attributes[attr] = rewriteUrl(node.attributes[attr], prefix, secret);
|
|
114
|
-
}
|
|
115
|
-
if (node.attributes.srcset) {
|
|
116
|
-
node.attributes.srcset = node.attributes.srcset.split(",").map((entry) => {
|
|
117
|
-
const parts = entry.trim().split(SRCSET_SPLIT_RE);
|
|
118
|
-
const url = parts[0];
|
|
119
|
-
const descriptor = parts.slice(1).join(" ");
|
|
120
|
-
return url ? `${rewriteUrl(url, prefix, secret)}${descriptor ? ` ${descriptor}` : ""}` : entry;
|
|
121
|
-
}).join(", ");
|
|
122
|
-
}
|
|
123
|
-
if (node.attributes.style)
|
|
124
|
-
node.attributes.style = rewriteUrlsInText(node.attributes.style, prefix, secret);
|
|
125
|
-
});
|
|
126
|
-
walkSync(ast, (node) => {
|
|
127
|
-
if (node.type === TEXT_NODE && node.value)
|
|
128
|
-
node.value = rewriteUrlsInText(node.value, prefix, secret);
|
|
129
|
-
});
|
|
130
|
-
let bodyNode = null;
|
|
131
|
-
walkSync(ast, (node) => {
|
|
132
|
-
if (node.type === ELEMENT_NODE && node.name === "body")
|
|
133
|
-
bodyNode = node;
|
|
134
|
-
});
|
|
135
|
-
const bodyHtml = bodyNode ? bodyNode.children.map((child) => renderSync(child)).join("") : renderSync(ast);
|
|
87
|
+
const { bodyHtml, cssUrls } = sanitizeInstagramEmbedHtml(html, prefix, secret);
|
|
136
88
|
const cssContents = await Promise.all(
|
|
137
89
|
cssUrls.map(
|
|
138
90
|
(url) => cachedCssFetch(url, {
|
|
139
91
|
headers: { Accept: "text/css" }
|
|
140
|
-
}).catch(() =>
|
|
92
|
+
}).catch(() => {
|
|
93
|
+
return "";
|
|
94
|
+
})
|
|
141
95
|
)
|
|
142
96
|
);
|
|
143
|
-
|
|
144
|
-
combinedCss = combinedCss.replace(
|
|
145
|
-
RSRC_RE,
|
|
146
|
-
(_m, path) => `url(${proxyAssetUrl(`https://static.cdninstagram.com/rsrc.php${path}`, prefix, secret)})`
|
|
147
|
-
);
|
|
148
|
-
combinedCss = rewriteUrlsInText(combinedCss, prefix, secret);
|
|
149
|
-
combinedCss = scopeCss(combinedCss, ".instagram-embed-root");
|
|
97
|
+
const combinedCss = sanitizeInstagramEmbedCss(cssContents.join("\n"), ".instagram-embed-root", prefix, secret);
|
|
150
98
|
const baseStyles = `
|
|
151
99
|
.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; }
|
|
152
100
|
.instagram-embed-root #splash-screen { display: none !important; }
|
|
@@ -157,5 +105,7 @@ export default withSigning(defineEventHandler(async (event) => {
|
|
|
157
105
|
${combinedCss}</style>${bodyHtml}</div>`;
|
|
158
106
|
setHeader(event, "Content-Type", "text/html");
|
|
159
107
|
setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
|
|
108
|
+
setHeader(event, "Content-Security-Policy", "sandbox; default-src 'none'");
|
|
109
|
+
setHeader(event, "X-Content-Type-Options", "nosniff");
|
|
160
110
|
return result;
|
|
161
111
|
}));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export declare const SKIP_REQUEST_HEADERS: Set<string>;
|
|
2
|
+
export declare function withResponseBodyIdleTimeout(body: ReadableStream<Uint8Array>, timeoutMs: number, onTimeout: () => void): ReadableStream<Uint8Array>;
|
|
2
3
|
/**
|
|
3
4
|
* Privacy-aware proxy handler for first-party script collection endpoints.
|
|
4
5
|
* Routes requests to third-party analytics while protecting user privacy.
|
|
5
6
|
*/
|
|
6
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<
|
|
7
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void | null>>;
|
|
7
8
|
export default _default;
|