@nuxt/scripts 1.0.0-rc.9 → 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 +6 -2
  39. package/dist/runtime/registry/bing-uet.js +13 -1
  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 +104 -11
  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/bd58b869-1eb5-4c50-871c-707f9b71e8f9.json +0 -1
  97. package/dist/devtools-client/_nuxt/index.CA-OpSj0.css +0 -1
@@ -1,4 +1,16 @@
1
1
  import { any, array, boolean, custom, literal, minLength, number, object, optional, pipe, record, string, union } from "valibot";
2
+ const consentCategoryValue = union([literal("granted"), literal("denied")]);
3
+ const gcmConsentState = object({
4
+ ad_storage: optional(consentCategoryValue),
5
+ ad_user_data: optional(consentCategoryValue),
6
+ ad_personalization: optional(consentCategoryValue),
7
+ analytics_storage: optional(consentCategoryValue),
8
+ functionality_storage: optional(consentCategoryValue),
9
+ personalization_storage: optional(consentCategoryValue),
10
+ security_storage: optional(consentCategoryValue),
11
+ wait_for_update: optional(number()),
12
+ region: optional(array(string()))
13
+ });
2
14
  export const BlueskyEmbedOptions = object({
3
15
  /**
4
16
  * The Bluesky post URL to embed.
@@ -21,7 +33,14 @@ export const ClarityOptions = object({
21
33
  * The Clarity token.
22
34
  * @see https://learn.microsoft.com/en-us/clarity/setup-clarity
23
35
  */
24
- id: pipe(string(), minLength(10))
36
+ id: pipe(string(), minLength(10)),
37
+ /**
38
+ * Default consent state applied before Clarity starts.
39
+ * - `boolean` - enable / disable cookies.
40
+ * - `Record<string, string>` - advanced consent vector (see Clarity docs).
41
+ * @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/cookie-consent
42
+ */
43
+ defaultConsent: optional(union([boolean(), record(string(), string())]))
25
44
  });
26
45
  export const CloudflareWebAnalyticsOptions = object({
27
46
  /**
@@ -264,7 +283,12 @@ export const GoogleAnalyticsOptions = object({
264
283
  * @default 'dataLayer'
265
284
  * @see https://developers.google.com/analytics/devguides/collection/gtagjs/setting-up-gtag#rename_the_data_layer
266
285
  */
267
- l: optional(string())
286
+ l: optional(string()),
287
+ /**
288
+ * Default GCMv2 consent state fired as `gtag('consent', 'default', ...)` before `gtag('js', ...)`.
289
+ * @see https://developers.google.com/tag-platform/security/guides/consent
290
+ */
291
+ defaultConsent: optional(gcmConsentState)
268
292
  });
269
293
  export const GoogleMapsOptions = object({
270
294
  /**
@@ -527,14 +551,28 @@ export const MatomoAnalyticsOptions = object({
527
551
  * Automatically track page views on route change.
528
552
  * @default true
529
553
  */
530
- watch: optional(boolean())
554
+ watch: optional(boolean()),
555
+ /**
556
+ * Default tracking-consent state applied BEFORE the tracker is initialised.
557
+ * - `'required'` — call `requireConsent` without granting (user must opt in later).
558
+ * - `'given'` — call `requireConsent` then `setConsentGiven`.
559
+ * - `'not-required'` — no consent gating (default Matomo behaviour).
560
+ * @see https://developer.matomo.org/guides/tracking-consent
561
+ */
562
+ defaultConsent: optional(union([literal("required"), literal("given"), literal("not-required")]))
531
563
  });
532
564
  export const MetaPixelOptions = object({
533
565
  /**
534
566
  * Your Meta (Facebook) Pixel ID.
535
567
  * @see https://developers.facebook.com/docs/meta-pixel/get-started
536
568
  */
537
- id: union([string(), number()])
569
+ id: union([string(), number()]),
570
+ /**
571
+ * Default consent state. `'granted'` fires `fbq('consent', 'grant')`,
572
+ * `'denied'` fires `fbq('consent', 'revoke')`, both called before `fbq('init', id)`.
573
+ * @see https://www.facebook.com/business/help/1151321516677370
574
+ */
575
+ defaultConsent: optional(union([literal("granted"), literal("denied")]))
538
576
  });
539
577
  export const NpmOptions = object({
540
578
  /**
@@ -627,7 +665,14 @@ export const PostHogOptions = object({
627
665
  * Additional PostHog configuration options passed directly to `posthog.init()`.
628
666
  * @see https://posthog.com/docs/libraries/js#config
629
667
  */
630
- config: optional(record(string(), any()))
668
+ config: optional(record(string(), any())),
669
+ /**
670
+ * Default capture-consent state for PostHog.
671
+ * - `'opt-out'`: passed as `opt_out_capturing_by_default: true` to `posthog.init`, so capturing is suppressed from the first event.
672
+ * - `'opt-in'`: applied after `posthog.init` via `posthog.opt_in_capturing()` on the returned instance.
673
+ * @see https://posthog.com/docs/privacy/opting-out
674
+ */
675
+ defaultConsent: optional(union([literal("opt-in"), literal("opt-out")]))
631
676
  });
632
677
  export const RedditPixelOptions = object({
633
678
  /**
@@ -700,7 +745,14 @@ export const MixpanelAnalyticsOptions = object({
700
745
  * Your Mixpanel project token.
701
746
  * @see https://docs.mixpanel.com/docs/tracking-methods/sdks/javascript#1-initialize-the-library
702
747
  */
703
- token: string()
748
+ token: string(),
749
+ /**
750
+ * Default tracking-consent state for Mixpanel.
751
+ * - `'opt-out'`: passed as `opt_out_tracking_by_default: true` to `mixpanel.init`, so tracking is suppressed from the first call.
752
+ * - `'opt-in'`: queued via `mixpanel.push(['opt_in_tracking'])` so the real SDK runs it immediately after load.
753
+ * @see https://docs.mixpanel.com/docs/privacy/opt-out-of-tracking
754
+ */
755
+ defaultConsent: optional(union([literal("opt-in"), literal("opt-out")]))
704
756
  });
705
757
  export const BingUetOptions = object({
706
758
  /**
@@ -712,7 +764,14 @@ export const BingUetOptions = object({
712
764
  * Enable automatic SPA page tracking.
713
765
  * @default true
714
766
  */
715
- enableAutoSpaTracking: optional(boolean())
767
+ enableAutoSpaTracking: optional(boolean()),
768
+ /**
769
+ * Default consent state fired as `uetq.push('consent', 'default', ...)` before UET init.
770
+ * @see https://help.ads.microsoft.com/#apex/ads/en/60119/1-500
771
+ */
772
+ defaultConsent: optional(object({
773
+ ad_storage: optional(consentCategoryValue)
774
+ }))
716
775
  });
717
776
  export const SegmentOptions = object({
718
777
  /**
@@ -807,7 +866,15 @@ export const TikTokPixelOptions = object({
807
866
  * Whether to automatically track a page view on initialization.
808
867
  * @default true
809
868
  */
810
- trackPageView: optional(boolean())
869
+ trackPageView: optional(boolean()),
870
+ /**
871
+ * Default consent state, applied before `ttq('init', id)`.
872
+ * - `'granted'` fires `ttq.grantConsent()`
873
+ * - `'denied'` fires `ttq.revokeConsent()`
874
+ * - `'hold'` fires `ttq.holdConsent()` to defer until an explicit update
875
+ * @see https://business-api.tiktok.com/portal/docs?id=1739585600931842
876
+ */
877
+ defaultConsent: optional(union([literal("granted"), literal("denied"), literal("hold")]))
811
878
  });
812
879
  export const UmamiAnalyticsOptions = object({
813
880
  /**
@@ -1,4 +1,4 @@
1
- import type { RegistryScriptInput } from '#nuxt-scripts/types';
1
+ import type { RegistryScriptInput, UseScriptContext } from '#nuxt-scripts/types';
2
2
  import { TikTokPixelOptions } from './schemas.js';
3
3
  type StandardEvents = 'ViewContent' | 'ClickButton' | 'Search' | 'AddToWishlist' | 'AddToCart' | 'InitiateCheckout' | 'AddPaymentInfo' | 'CompletePayment' | 'PlaceAnOrder' | 'Contact' | 'Download' | 'SubmitForm' | 'CompleteRegistration' | 'Subscribe';
4
4
  interface EventProperties {
@@ -29,6 +29,12 @@ export interface TikTokPixelApi {
29
29
  push: TtqFns;
30
30
  loaded: boolean;
31
31
  queue: any[];
32
+ /** Opt user in to tracking. Queued before the script loads; live once `events.js` binds. */
33
+ grantConsent: () => void;
34
+ /** Opt user out of tracking. Queued before the script loads; live once `events.js` binds. */
35
+ revokeConsent: () => void;
36
+ /** Defer consent until an explicit grant/revoke. Queued before the script loads; live once `events.js` binds. */
37
+ holdConsent: () => void;
32
38
  };
33
39
  }
34
40
  declare global {
@@ -38,4 +44,12 @@ declare global {
38
44
  }
39
45
  export { TikTokPixelOptions };
40
46
  export type TikTokPixelInput = RegistryScriptInput<typeof TikTokPixelOptions, true, false>;
41
- export declare function useScriptTikTokPixel<T extends TikTokPixelApi>(_options?: TikTokPixelInput): import("#nuxt-scripts/types").UseScriptContext<T>;
47
+ export interface TikTokPixelConsent {
48
+ /** Call `ttq.grantConsent()`. */
49
+ grant: () => void;
50
+ /** Call `ttq.revokeConsent()`. */
51
+ revoke: () => void;
52
+ /** Call `ttq.holdConsent()` to defer the decision. */
53
+ hold: () => void;
54
+ }
55
+ export declare function useScriptTikTokPixel<T extends TikTokPixelApi>(_options?: TikTokPixelInput): UseScriptContext<T, TikTokPixelConsent>;
@@ -3,7 +3,7 @@ import { useRegistryScript } from "../utils.js";
3
3
  import { TikTokPixelOptions } from "./schemas.js";
4
4
  export { TikTokPixelOptions };
5
5
  export function useScriptTikTokPixel(_options) {
6
- return useRegistryScript("tiktokPixel", (options) => ({
6
+ const instance = useRegistryScript("tiktokPixel", (options) => ({
7
7
  scriptInput: {
8
8
  src: withQuery("https://analytics.tiktok.com/i18n/pixel/events.js", {
9
9
  sdkid: options?.id,
@@ -29,6 +29,19 @@ export function useScriptTikTokPixel(_options) {
29
29
  ttq.push = ttq;
30
30
  ttq.loaded = true;
31
31
  ttq.queue = [];
32
+ const consentMethods = ["grantConsent", "revokeConsent", "holdConsent"];
33
+ for (const name of consentMethods) {
34
+ ;
35
+ ttq[name] = function(...params) {
36
+ ttq.queue.push([name, ...params]);
37
+ };
38
+ }
39
+ if (options?.defaultConsent === "granted")
40
+ ttq.grantConsent();
41
+ else if (options?.defaultConsent === "denied")
42
+ ttq.revokeConsent();
43
+ else if (options?.defaultConsent === "hold")
44
+ ttq.holdConsent();
32
45
  if (options?.id) {
33
46
  ttq("init", options.id);
34
47
  if (options?.trackPageView !== false) {
@@ -37,4 +50,12 @@ export function useScriptTikTokPixel(_options) {
37
50
  }
38
51
  }
39
52
  }), _options);
53
+ if (import.meta.client && !instance.consent) {
54
+ instance.consent = {
55
+ grant: () => instance.proxy.ttq.grantConsent(),
56
+ revoke: () => instance.proxy.ttq.revokeConsent(),
57
+ hold: () => instance.proxy.ttq.holdConsent()
58
+ };
59
+ }
60
+ return instance;
40
61
  }
@@ -49,10 +49,6 @@ export interface XEmbedTweetData {
49
49
  };
50
50
  }
51
51
  export type XEmbedInput = RegistryScriptInput<typeof XEmbedOptions, false, false>;
52
- /**
53
- * Proxy an X/Twitter image URL through the server
54
- */
55
- export declare function proxyXImageUrl(url: string, proxyEndpoint?: string): string;
56
52
  /**
57
53
  * Format a tweet date for display
58
54
  */
@@ -1,9 +1,5 @@
1
1
  import { XEmbedOptions } from "./schemas.js";
2
2
  export { XEmbedOptions };
3
- export function proxyXImageUrl(url, proxyEndpoint = "/_scripts/embed/x-image") {
4
- const separator = proxyEndpoint.includes("?") ? "&" : "?";
5
- return `${proxyEndpoint}${separator}url=${encodeURIComponent(url)}`;
6
- }
7
3
  export function formatTweetDate(dateString) {
8
4
  const date = new Date(dateString);
9
5
  const time = date.toLocaleString("en-US", {
@@ -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,16 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
- uri: string;
3
- cid: string;
4
- author: Record<string, any>;
5
- record: Record<string, any>;
6
- embed?: Record<string, any>;
7
- likeCount: number;
8
- repostCount: number;
9
- replyCount: number;
10
- quoteCount: number;
11
- indexedAt: string;
12
- labels: Array<{
13
- val: string;
14
- }>;
15
- }>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Record<string, any>>>;
16
2
  export default _default;
@@ -1,7 +1,20 @@
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 { rewriteBlueskyPostImages } from "./utils/embed-rewriters.js";
3
5
  import { withSigning } from "./utils/withSigning.js";
4
6
  const BSKY_POST_URL_RE = /^https:\/\/bsky\.app\/profile\/([^/]+)\/post\/([^/?]+)$/;
7
+ const EMBED_BSKY_SUFFIX_RE = /\/embed\/bluesky$/;
8
+ const cachedProfileFetch = createCachedJsonFetch(
9
+ "nuxt-scripts-bsky-profile",
10
+ 86400,
11
+ (url) => url
12
+ );
13
+ const cachedPostFetch = createCachedJsonFetch(
14
+ "nuxt-scripts-bsky-post",
15
+ 600,
16
+ (url) => url
17
+ );
5
18
  export default withSigning(defineEventHandler(async (event) => {
6
19
  const query = getQuery(event);
7
20
  const postUrl = query.url;
@@ -21,7 +34,7 @@ export default withSigning(defineEventHandler(async (event) => {
21
34
  const [, actor, rkey] = match;
22
35
  let did = actor;
23
36
  if (!actor.startsWith("did:")) {
24
- const profile = await $fetch(
37
+ const profile = await cachedProfileFetch(
25
38
  `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(actor)}`
26
39
  ).catch((error) => {
27
40
  throw createError({
@@ -32,7 +45,7 @@ export default withSigning(defineEventHandler(async (event) => {
32
45
  did = profile.did;
33
46
  }
34
47
  const uri = `at://${did}/app.bsky.feed.post/${rkey}`;
35
- const response = await $fetch(
48
+ const response = await cachedPostFetch(
36
49
  `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${encodeURIComponent(uri)}&depth=0&parentHeight=0`
37
50
  ).catch((error) => {
38
51
  throw createError({
@@ -46,7 +59,7 @@ export default withSigning(defineEventHandler(async (event) => {
46
59
  statusMessage: "Post not found"
47
60
  });
48
61
  }
49
- const post = response.thread.post;
62
+ const post = structuredClone(response.thread.post);
50
63
  const hasOptOut = post.labels?.some((l) => l.val === "!no-unauthenticated") || post.author?.labels?.some((l) => l.val === "!no-unauthenticated");
51
64
  if (hasOptOut) {
52
65
  throw createError({
@@ -54,6 +67,11 @@ export default withSigning(defineEventHandler(async (event) => {
54
67
  statusMessage: "Author has opted out of external embedding"
55
68
  });
56
69
  }
70
+ const handlerPath = event.path?.split("?")[0] || "";
71
+ const prefix = handlerPath.replace(EMBED_BSKY_SUFFIX_RE, "") || "/_scripts";
72
+ const imagePath = `${prefix}/embed/bluesky-image`;
73
+ const secret = useRuntimeConfig(event)["nuxt-scripts"]?.proxySecret;
74
+ rewriteBlueskyPostImages(post, imagePath, secret);
57
75
  setHeader(event, "Content-Type", "application/json");
58
76
  setHeader(event, "Cache-Control", "public, max-age=600, s-maxage=600");
59
77
  return post;
@@ -1,8 +1,13 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
- import { $fetch } from "ofetch";
4
3
  import { withQuery } from "ufo";
4
+ import { createCachedJsonFetch } from "./utils/cached-upstream.js";
5
5
  import { withSigning } from "./utils/withSigning.js";
6
+ const cachedGeocodeFetch = createCachedJsonFetch(
7
+ "nuxt-scripts-geocode",
8
+ 2592e3,
9
+ (url) => url
10
+ );
6
11
  export default withSigning(defineEventHandler(async (event) => {
7
12
  const runtimeConfig = useRuntimeConfig();
8
13
  const privateConfig = runtimeConfig["nuxt-scripts"]?.googleMapsGeocodeProxy;
@@ -19,10 +24,8 @@ export default withSigning(defineEventHandler(async (event) => {
19
24
  ...safeQuery,
20
25
  key: apiKey
21
26
  });
22
- const data = await $fetch(geocodeUrl, {
23
- headers: {
24
- "User-Agent": "Nuxt Scripts Google Geocode Proxy"
25
- }
27
+ const data = await cachedGeocodeFetch(geocodeUrl, {
28
+ headers: { "User-Agent": "Nuxt Scripts Google Geocode Proxy" }
26
29
  }).catch((error) => {
27
30
  throw createError({
28
31
  statusCode: error.statusCode || 500,
@@ -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,8 +1,11 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
- import { $fetch } from "ofetch";
4
3
  import { withQuery } from "ufo";
4
+ import { createCachedBinaryFetch } from "./utils/cached-upstream.js";
5
+ import { PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, SIG_PARAM } from "./utils/sign-constants.js";
5
6
  import { withSigning } from "./utils/withSigning.js";
7
+ const cachedMapFetch = createCachedBinaryFetch("nuxt-scripts-static-map", 604800);
8
+ const STRIP_PARAMS = /* @__PURE__ */ new Set([SIG_PARAM, PAGE_TOKEN_PARAM, PAGE_TOKEN_TS_PARAM, "key"]);
6
9
  export default withSigning(defineEventHandler(async (event) => {
7
10
  const runtimeConfig = useRuntimeConfig();
8
11
  const publicConfig = runtimeConfig.public["nuxt-scripts"]?.googleStaticMapsProxy;
@@ -21,15 +24,17 @@ export default withSigning(defineEventHandler(async (event) => {
21
24
  });
22
25
  }
23
26
  const query = getQuery(event);
24
- const { key: _clientKey, ...safeQuery } = query;
27
+ const safeQuery = {};
28
+ for (const [k, v] of Object.entries(query)) {
29
+ if (!STRIP_PARAMS.has(k))
30
+ safeQuery[k] = v;
31
+ }
25
32
  const googleMapsUrl = withQuery("https://maps.googleapis.com/maps/api/staticmap", {
26
33
  ...safeQuery,
27
34
  key: apiKey
28
35
  });
29
- const response = await $fetch.raw(googleMapsUrl, {
30
- headers: {
31
- "User-Agent": "Nuxt Scripts Google Static Maps Proxy"
32
- }
36
+ const result = await cachedMapFetch(googleMapsUrl, {
37
+ headers: { "User-Agent": "Nuxt Scripts Google Static Maps Proxy" }
33
38
  }).catch((error) => {
34
39
  throw createError({
35
40
  statusCode: error.statusCode || 500,
@@ -37,8 +42,8 @@ export default withSigning(defineEventHandler(async (event) => {
37
42
  });
38
43
  });
39
44
  const cacheMaxAge = publicConfig.cacheMaxAge || 3600;
40
- setHeader(event, "Content-Type", response.headers.get("content-type") || "image/png");
45
+ setHeader(event, "Content-Type", result.contentType || "image/png");
41
46
  setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
42
47
  setHeader(event, "Vary", "Accept-Encoding");
43
- return response._data;
48
+ return result.body;
44
49
  }));
@@ -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,8 +1,9 @@
1
1
  import { createError, defineEventHandler, getQuery, setHeader } from "h3";
2
2
  import { useRuntimeConfig } from "nitropack/runtime";
3
- import { $fetch } from "ofetch";
4
3
  import { withQuery } from "ufo";
4
+ import { createCachedBinaryFetch } from "./utils/cached-upstream.js";
5
5
  import { withSigning } from "./utils/withSigning.js";
6
+ const cachedGravatarFetch = createCachedBinaryFetch("nuxt-scripts-gravatar", 3600);
6
7
  export default withSigning(defineEventHandler(async (event) => {
7
8
  const runtimeConfig = useRuntimeConfig();
8
9
  const proxyConfig = runtimeConfig.public["nuxt-scripts"]?.gravatarProxy;
@@ -29,10 +30,8 @@ export default withSigning(defineEventHandler(async (event) => {
29
30
  d: defaultImg,
30
31
  r: rating
31
32
  });
32
- const response = await $fetch.raw(gravatarUrl, {
33
- headers: {
34
- "User-Agent": "Nuxt Scripts Gravatar Proxy"
35
- }
33
+ const result = await cachedGravatarFetch(gravatarUrl, {
34
+ headers: { "User-Agent": "Nuxt Scripts Gravatar Proxy" }
36
35
  }).catch((error) => {
37
36
  throw createError({
38
37
  statusCode: error.statusCode || 500,
@@ -40,8 +39,8 @@ export default withSigning(defineEventHandler(async (event) => {
40
39
  });
41
40
  });
42
41
  const cacheMaxAge = proxyConfig?.cacheMaxAge ?? 3600;
43
- setHeader(event, "Content-Type", response.headers.get("content-type") || "image/jpeg");
42
+ setHeader(event, "Content-Type", result.contentType || "image/jpeg");
44
43
  setHeader(event, "Cache-Control", `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`);
45
44
  setHeader(event, "Vary", "Accept-Encoding");
46
- return response._data;
45
+ return result.body;
47
46
  }));
@@ -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,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,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>;