nuxt-og-image 6.4.7 → 6.4.9

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 (94) hide show
  1. package/dist/chunks/tw4.cjs +1 -1
  2. package/dist/chunks/tw4.mjs +1 -1
  3. package/dist/chunks/uno.cjs +1 -1
  4. package/dist/chunks/uno.mjs +1 -1
  5. package/dist/devtools/200.html +1 -1
  6. package/dist/devtools/404.html +1 -1
  7. package/dist/devtools/_nuxt/Axr0GNgb.js +23 -0
  8. package/dist/devtools/_nuxt/BFnUFv7a.js +6 -0
  9. package/dist/devtools/_nuxt/BLdiDWsd.js +3 -0
  10. package/dist/devtools/_nuxt/BPuj2Ioi.js +1 -0
  11. package/dist/devtools/_nuxt/BhUO5EEy.js +4 -0
  12. package/dist/devtools/_nuxt/{C2qZkcnS.js → Bu77Nt5h.js} +1 -1
  13. package/dist/devtools/_nuxt/CbxDj8dt.js +2 -0
  14. package/dist/devtools/_nuxt/CeQOmnUG.js +1 -0
  15. package/dist/devtools/_nuxt/Ceo32Wod.js +1 -0
  16. package/dist/devtools/_nuxt/{B75jVnAo.js → D-SbM8XJ.js} +1 -1
  17. package/dist/devtools/_nuxt/DWbsyffp.js +152 -0
  18. package/dist/devtools/_nuxt/Dd8dEdTA.js +3 -0
  19. package/dist/devtools/_nuxt/DevtoolsSection.BsOFzz12.css +1 -0
  20. package/dist/devtools/_nuxt/DevtoolsSnippet.DTkjQmsJ.css +1 -0
  21. package/dist/devtools/_nuxt/{IFrameLoader.HTWy4e5S.css → IFrameLoader.Bd09J70F.css} +1 -1
  22. package/dist/devtools/_nuxt/builds/latest.json +1 -1
  23. package/dist/devtools/_nuxt/builds/meta/df7e4f86-26df-4aa8-839c-9d7fb31dd16b.json +1 -0
  24. package/dist/devtools/_nuxt/entry.D7nBa7-d.css +2 -0
  25. package/dist/devtools/_nuxt/{D5adIWRn.js → oq4DzcWM.js} +1 -1
  26. package/dist/devtools/_nuxt/{pages.DNfeVIDd.css → pages.D6uci9iO.css} +1 -1
  27. package/dist/devtools/_nuxt/renderer-select.Pd5oWk7V.css +1 -0
  28. package/dist/devtools/_nuxt/{templates.C7e57cTp.css → templates.BHO7w0UG.css} +1 -1
  29. package/dist/devtools/debug/index.html +1 -1
  30. package/dist/devtools/docs/index.html +1 -1
  31. package/dist/devtools/index.html +1 -1
  32. package/dist/devtools/templates/index.html +1 -1
  33. package/dist/module.cjs +1 -1
  34. package/dist/module.json +1 -1
  35. package/dist/module.mjs +1 -1
  36. package/dist/runtime/app/client-utils.js +1 -1
  37. package/dist/runtime/app/components/Templates/Community/SimpleBlog.satori.vue +1 -1
  38. package/dist/runtime/app/utils/plugins.js +4 -4
  39. package/dist/runtime/app/utils.js +1 -1
  40. package/dist/runtime/server/og-image/bindings/browser/chrome-launcher.js +2 -1
  41. package/dist/runtime/server/og-image/bindings/browser/cloudflare.js +4 -2
  42. package/dist/runtime/server/og-image/bindings/browser/on-demand.js +2 -1
  43. package/dist/runtime/server/og-image/bindings/browser/playwright.js +2 -1
  44. package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +1 -1
  45. package/dist/runtime/server/og-image/bindings/font-assets/node.js +1 -1
  46. package/dist/runtime/server/og-image/browser/renderer.js +1 -1
  47. package/dist/runtime/server/og-image/browser/screenshot.js +2 -2
  48. package/dist/runtime/server/og-image/cache/buildCache.js +1 -1
  49. package/dist/runtime/server/og-image/context.js +12 -5
  50. package/dist/runtime/server/og-image/core/plugins/imageSrc.js +30 -59
  51. package/dist/runtime/server/og-image/core/transforms/emojis/fetch.js +1 -1
  52. package/dist/runtime/server/og-image/core/vnodes.js +3 -2
  53. package/dist/runtime/server/og-image/devtools.js +1 -1
  54. package/dist/runtime/server/og-image/font-source.d.ts +20 -0
  55. package/dist/runtime/server/og-image/font-source.js +20 -0
  56. package/dist/runtime/server/og-image/fonts.d.ts +8 -1
  57. package/dist/runtime/server/og-image/fonts.js +22 -32
  58. package/dist/runtime/server/og-image/satori/plugins/emojis.js +1 -1
  59. package/dist/runtime/server/og-image/satori/renderer.js +9 -3
  60. package/dist/runtime/server/og-image/takumi/nodes.js +1 -1
  61. package/dist/runtime/server/og-image/takumi/renderer.js +76 -24
  62. package/dist/runtime/server/og-image/templates/html.js +3 -2
  63. package/dist/runtime/server/plugins/prerender.js +2 -2
  64. package/dist/runtime/server/routes/debug.json.js +2 -2
  65. package/dist/runtime/server/routes/resolve.js +2 -2
  66. package/dist/runtime/server/util/eventHandlers.js +3 -2
  67. package/dist/runtime/server/util/fetchLocalAsset.js +1 -1
  68. package/dist/runtime/server/util/kit.d.ts +1 -1
  69. package/dist/runtime/server/util/kit.js +5 -2
  70. package/dist/runtime/server/util/options.js +1 -1
  71. package/dist/runtime/server/util/ssrf.d.ts +27 -0
  72. package/dist/runtime/server/util/ssrf.js +165 -0
  73. package/dist/runtime/server/util/withTimeout.d.ts +1 -0
  74. package/dist/runtime/server/util/withTimeout.js +12 -0
  75. package/dist/runtime/server/utils.js +6 -2
  76. package/dist/shared/{nuxt-og-image.Cr3WHMk1.mjs → nuxt-og-image.CW3PwVsO.mjs} +7 -2
  77. package/dist/shared/{nuxt-og-image.TJuh6pW5.cjs → nuxt-og-image.DnwTVrnM.cjs} +8 -3
  78. package/package.json +18 -17
  79. package/types/virtual.d.ts +1 -1
  80. package/dist/devtools/_nuxt/671cdwR5.js +0 -1
  81. package/dist/devtools/_nuxt/8kkJjsM6.js +0 -2
  82. package/dist/devtools/_nuxt/BJ9iyiD_.js +0 -3
  83. package/dist/devtools/_nuxt/BcGh1UWn.js +0 -23
  84. package/dist/devtools/_nuxt/CHmOAOu6.js +0 -3
  85. package/dist/devtools/_nuxt/Ci8ZvNRB.js +0 -1
  86. package/dist/devtools/_nuxt/DVtbGlya.js +0 -152
  87. package/dist/devtools/_nuxt/DbOyl855.js +0 -1
  88. package/dist/devtools/_nuxt/DevtoolsSection.DTZAmexP.css +0 -1
  89. package/dist/devtools/_nuxt/DevtoolsSnippet.Cd7XR-3f.css +0 -1
  90. package/dist/devtools/_nuxt/DrqfEi04.js +0 -4
  91. package/dist/devtools/_nuxt/builds/meta/d8fa30f4-68dd-4f57-a1c2-1c3a852f7ee7.json +0 -1
  92. package/dist/devtools/_nuxt/cIRSrPLM.js +0 -6
  93. package/dist/devtools/_nuxt/entry.zgUFdiSv.css +0 -2
  94. package/dist/devtools/_nuxt/renderer-select.elTTxv30.css +0 -1
@@ -3,6 +3,8 @@ import { withBase } from "ufo";
3
3
  import { logger } from "../../../logger.js";
4
4
  import { fetchLocalAsset } from "../../util/fetchLocalAsset.js";
5
5
  import { getFetchTimeout } from "../../util/fetchTimeout.js";
6
+ import { fetchWithRedirectValidation, isBlockedUrl } from "../../util/ssrf.js";
7
+ import { withTimeout } from "../../util/withTimeout.js";
6
8
  import { buildSubsetFamilyChain, extractCodepoints, getDefaultFontFamily, loadFontsForRenderer, resolveSubsetChain } from "../fonts.js";
7
9
  import { getExtractResourceUrls, getTakumi } from "./instances.js";
8
10
  import { createTakumiNodes } from "./nodes.js";
@@ -20,10 +22,45 @@ async function getTakumiState(event) {
20
22
  };
21
23
  return nitro._takumiState;
22
24
  }
23
- function withTakumiLock(state, fn) {
24
- const next = state.lock.then(fn, fn);
25
- state.lock = next.catch(() => void 0);
26
- return next;
25
+ function withTakumiLock(state, timeoutMs, fn, onLockTimeout) {
26
+ const guarded = async () => {
27
+ let timer;
28
+ try {
29
+ return await Promise.race([
30
+ fn(),
31
+ new Promise((_, reject) => {
32
+ timer = setTimeout(
33
+ () => reject(new Error(`takumi render timed out after ${timeoutMs}ms (lock-held)`)),
34
+ timeoutMs
35
+ );
36
+ })
37
+ ]);
38
+ } catch (err) {
39
+ try {
40
+ const Renderer = await getTakumi();
41
+ state.renderer = new Renderer();
42
+ state.loadedFontKeys.clear();
43
+ state.loadedFamilies.clear();
44
+ } catch (resetErr) {
45
+ logger.warn(`failed to reset takumi renderer after lock timeout: ${resetErr?.message || resetErr}`);
46
+ }
47
+ throw err;
48
+ } finally {
49
+ clearTimeout(timer);
50
+ }
51
+ };
52
+ const work = state.lock.then(guarded, guarded);
53
+ state.lock = work.catch(() => void 0);
54
+ let acquireTimer;
55
+ return Promise.race([
56
+ work,
57
+ new Promise((_, reject) => {
58
+ acquireTimer = setTimeout(() => {
59
+ onLockTimeout?.();
60
+ reject(new Error(`takumi lock acquire timed out after ${timeoutMs}ms`));
61
+ }, timeoutMs);
62
+ })
63
+ ]).finally(() => clearTimeout(acquireTimer));
27
64
  }
28
65
  function dedupeFontsByBinary(fonts) {
29
66
  const byBinary = /* @__PURE__ */ new Map();
@@ -116,11 +153,16 @@ async function createImage(event, format) {
116
153
  const { fontFamilyOverride, defaultFont } = getDefaultFontFamily(options);
117
154
  const nodes = await createTakumiNodes(event);
118
155
  const codepoints = extractCodepoints(nodes);
119
- const fonts = await timings.measure("font-load", () => loadFontsForRenderer(event, { supportedFormats: /* @__PURE__ */ new Set(["ttf", "woff2"]), component: options.component, fontFamilyOverride: fontFamilyOverride || defaultFont, codepoints }));
120
- await event._nitro.hooks.callHook("nuxt-og-image:takumi:nodes", nodes, event);
156
+ const fonts = await timings.measure("font-load", () => loadFontsForRenderer(event, { supportedFormats: /* @__PURE__ */ new Set(["ttf", "woff2"]), preferStatic: true, component: options.component, fontFamilyOverride: fontFamilyOverride || defaultFont, codepoints }));
157
+ const hookTimeout = event.runtimeConfig.security?.renderTimeout ?? 15e3;
158
+ await withTimeout(
159
+ event._nitro.hooks.callHook("nuxt-og-image:takumi:nodes", nodes, event),
160
+ hookTimeout,
161
+ "nuxt-og-image:takumi:nodes hook"
162
+ );
121
163
  const subsetChains = buildSubsetFamilyChain(fonts);
122
- const state = await getTakumiState(event);
123
- const extractResourceUrls = await getExtractResourceUrls();
164
+ const state = await timings.measure("takumi-init", () => getTakumiState(event));
165
+ const extractResourceUrls = await timings.measure("takumi-extract-init", () => getExtractResourceUrls());
124
166
  const resourceUrls = await extractResourceUrls(nodes);
125
167
  const baseURL = event.runtimeConfig.app.baseURL;
126
168
  const fetchedResources = [];
@@ -136,13 +178,18 @@ async function createImage(event, format) {
136
178
  headers,
137
179
  includeExternalFallback: true
138
180
  });
139
- } else {
181
+ } else if (import.meta.dev) {
140
182
  data = await $fetch(src, {
141
183
  responseType: "arrayBuffer",
142
184
  signal: AbortSignal.timeout(fetchTimeout),
143
185
  timeout: fetchTimeout,
144
186
  headers
145
187
  }).catch(() => void 0);
188
+ } else if (!isBlockedUrl(src)) {
189
+ data = await fetchWithRedirectValidation(src, {
190
+ timeout: fetchTimeout,
191
+ headers
192
+ }) ?? void 0;
146
193
  }
147
194
  if (data)
148
195
  fetchedResources.push({ src, data: new Uint8Array(data) });
@@ -158,21 +205,26 @@ async function createImage(event, format) {
158
205
  fetchedResources,
159
206
  devicePixelRatio: dpr
160
207
  });
161
- return await withTakumiLock(state, () => timings.measure("render-takumi", async () => {
162
- await loadFontsIntoRenderer(state, fonts);
163
- const rootStyle = nodes.style ?? {};
164
- if (fontFamilyOverride) {
165
- const chain = subsetChains.get(fontFamilyOverride);
166
- if (chain) {
167
- rootStyle.fontFamily = chain.map((f) => `"${f}"`).join(", ");
168
- } else if (state.loadedFamilies.has(fontFamilyOverride)) {
169
- rootStyle.fontFamily = fontFamilyOverride;
208
+ const lockTimeout = event.runtimeConfig.security?.renderTimeout ?? 15e3;
209
+ const endLockWait = timings.start("lock-wait");
210
+ return await withTakumiLock(state, lockTimeout, () => {
211
+ endLockWait();
212
+ return timings.measure("render-takumi", async () => {
213
+ await loadFontsIntoRenderer(state, fonts);
214
+ const rootStyle = nodes.style ?? {};
215
+ if (fontFamilyOverride) {
216
+ const chain = subsetChains.get(fontFamilyOverride);
217
+ if (chain) {
218
+ rootStyle.fontFamily = chain.map((f) => `"${f}"`).join(", ");
219
+ } else if (state.loadedFamilies.has(fontFamilyOverride)) {
220
+ rootStyle.fontFamily = fontFamilyOverride;
221
+ }
170
222
  }
171
- }
172
- nodes.style = rootStyle;
173
- rewriteFontFamilies(nodes, state.loadedFamilies, subsetChains);
174
- return state.renderer.render(nodes, renderOptions);
175
- }));
223
+ nodes.style = rootStyle;
224
+ rewriteFontFamilies(nodes, state.loadedFamilies, subsetChains);
225
+ return state.renderer.render(nodes, renderOptions);
226
+ });
227
+ }, endLockWait);
176
228
  }
177
229
  const TakumiRenderer = {
178
230
  name: "takumi",
@@ -191,7 +243,7 @@ const TakumiRenderer = {
191
243
  async debug(e) {
192
244
  const [vnodes, fonts] = await Promise.all([
193
245
  createTakumiNodes(e),
194
- loadFontsForRenderer(e, { supportedFormats: /* @__PURE__ */ new Set(["ttf", "woff2"]), component: e.options.component })
246
+ loadFontsForRenderer(e, { supportedFormats: /* @__PURE__ */ new Set(["ttf", "woff2"]), preferStatic: true, component: e.options.component })
195
247
  ]);
196
248
  return {
197
249
  vnodes,
@@ -1,6 +1,6 @@
1
- import resolvedFonts from "#og-image/fonts";
2
1
  import { createHead, renderSSRHead } from "@unhead/vue/server";
3
2
  import { createError } from "h3";
3
+ import resolvedFonts from "#og-image/fonts";
4
4
  import { fetchIsland } from "../../util/kit.js";
5
5
  import { applyEmojis } from "../core/transforms/emojis/index.js";
6
6
  const RE_SCRIPT_TAG = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
@@ -13,7 +13,8 @@ export async function html(ctx) {
13
13
  statusMessage: `[Nuxt OG Image] Rendering an invalid component. Received options: ${JSON.stringify(options)}.`
14
14
  });
15
15
  }
16
- const island = await fetchIsland(ctx.e, ctx.options.component, typeof ctx.options.props !== "undefined" ? ctx.options.props : ctx.options);
16
+ const islandTimeout = ctx.runtimeConfig.security?.renderTimeout ?? 15e3;
17
+ const island = await fetchIsland(ctx.e, ctx.options.component, typeof ctx.options.props !== "undefined" ? ctx.options.props : ctx.options, islandTimeout);
17
18
  const head = createHead();
18
19
  head.push(island.head);
19
20
  let defaultFontFamily = "sans-serif";
@@ -1,9 +1,9 @@
1
- import { prerenderOptionsCache } from "#og-image-cache";
2
- import { createSitePathResolver } from "#site-config/server/composables/utils";
3
1
  import { parse } from "devalue";
4
2
  import { appendResponseHeader } from "h3";
5
3
  import { defineNitroPlugin } from "nitropack/runtime";
6
4
  import { parseURL } from "ufo";
5
+ import { prerenderOptionsCache } from "#og-image-cache";
6
+ import { createSitePathResolver } from "#site-config/server/composables/utils";
7
7
  import { isInternalRoute } from "../../shared.js";
8
8
  import { resolvePathCacheKey } from "../og-image/context.js";
9
9
  import { createNitroRouteRuleMatcher } from "../util/kit.js";
@@ -1,3 +1,4 @@
1
+ import { defineEventHandler, setHeader } from "h3";
1
2
  import { componentNames } from "#og-image-virtual/component-names.mjs";
2
3
  import compatibility from "#og-image/compatibility";
3
4
  import { fontRequirements } from "#og-image/font-requirements";
@@ -5,11 +6,10 @@ import resolvedFonts from "#og-image/fonts";
5
6
  import availableFonts from "#og-image/fonts-available";
6
7
  import { getNitroOrigin } from "#site-config/server/composables";
7
8
  import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
8
- import { defineEventHandler, setHeader } from "h3";
9
9
  import { useOgImageRuntimeConfig } from "../utils.js";
10
10
  export default defineEventHandler(async (e) => {
11
11
  setHeader(e, "Content-Type", "application/json");
12
- const runtimeConfig = useOgImageRuntimeConfig();
12
+ const runtimeConfig = useOgImageRuntimeConfig(e);
13
13
  return {
14
14
  siteConfigUrl: getSiteConfig(e).url,
15
15
  origin: getNitroOrigin(e),
@@ -1,6 +1,6 @@
1
- import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
2
1
  import { createError, defineEventHandler, getQuery, getRequestHost, sendRedirect } from "h3";
3
2
  import { parseURL, withLeadingSlash, withQuery } from "ufo";
3
+ import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
4
4
  import { isInternalRoute } from "../../shared.js";
5
5
  import { useOgImageRuntimeConfig } from "../utils.js";
6
6
  const RE_META_TAG = /<meta\b[^>]*>/gi;
@@ -30,7 +30,7 @@ function resolveTargetPath(event) {
30
30
  return stripped.replace(RE_IMAGE_EXT, "") || "/";
31
31
  }
32
32
  export default defineEventHandler(async (event) => {
33
- const runtimeConfig = useOgImageRuntimeConfig();
33
+ const runtimeConfig = useOgImageRuntimeConfig(event);
34
34
  const security = runtimeConfig.security;
35
35
  if (!import.meta.prerender && !import.meta.dev && security?.restrictRuntimeImagesToOrigin) {
36
36
  const requestHost = getRequestHost(event, { xForwardedHost: true });
@@ -1,5 +1,5 @@
1
- import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
2
1
  import { createError, getRequestHost, H3Error, setHeader } from "h3";
2
+ import { getSiteConfig } from "#site-config/server/composables/getSiteConfig";
3
3
  import { logger } from "../../logger.js";
4
4
  import { getBuildCachedImage, setBuildCachedImage } from "../og-image/cache/buildCache.js";
5
5
  import { resolveContext } from "../og-image/context.js";
@@ -16,6 +16,7 @@ export async function imageEventHandler(e) {
16
16
  if (ctx instanceof H3Error)
17
17
  return ctx;
18
18
  const timings = ctx.timings;
19
+ timings.record("resolve-context", performance.now() - reqStart);
19
20
  try {
20
21
  return await renderOgImage(e, ctx);
21
22
  } finally {
@@ -28,7 +29,7 @@ export async function imageEventHandler(e) {
28
29
  async function renderOgImage(e, ctx) {
29
30
  const timings = ctx.timings;
30
31
  const { isDevToolsContextRequest, extension, renderer } = ctx;
31
- const { debug, baseCacheKey, security } = useOgImageRuntimeConfig();
32
+ const { debug, baseCacheKey, security } = useOgImageRuntimeConfig(e);
32
33
  if (!import.meta.prerender && !import.meta.dev && security?.restrictRuntimeImagesToOrigin) {
33
34
  const requestHost = getRequestHost(e, { xForwardedHost: true });
34
35
  let requestHostname;
@@ -1,5 +1,5 @@
1
- import { getNitroOrigin } from "#site-config/server/composables";
2
1
  import { $fetch } from "ofetch";
2
+ import { getNitroOrigin } from "#site-config/server/composables";
3
3
  import { tryCloudflareAssetsFetch } from "./cloudflareAssets.js";
4
4
  export async function fetchLocalAsset(event, path, options) {
5
5
  const { fetchTimeout, headers, includeExternalFallback = false, onStepFailure } = options;
@@ -2,5 +2,5 @@ import type { H3Event } from 'h3';
2
2
  import type { NitroRouteRules } from 'nitropack';
3
3
  import type { NuxtIslandResponse } from 'nuxt/app';
4
4
  export { withoutQuery } from 'nuxtseo-shared/utils';
5
- export declare function fetchIsland(e: H3Event, component: string, props: Record<string, any>): Promise<NuxtIslandResponse>;
5
+ export declare function fetchIsland(e: H3Event, component: string, props: Record<string, any>, timeout?: number): Promise<NuxtIslandResponse>;
6
6
  export declare function createNitroRouteRuleMatcher(): ((path: string) => NitroRouteRules);
@@ -4,12 +4,15 @@ import { hash } from "ohash";
4
4
  import { createRouter as createRadixRouter, toRouteMatcher } from "radix3";
5
5
  import { withoutBase, withoutTrailingSlash } from "ufo";
6
6
  export { withoutQuery } from "nuxtseo-shared/utils";
7
- export function fetchIsland(e, component, props) {
7
+ export function fetchIsland(e, component, props, timeout) {
8
8
  const hashId = hash([component, props]).replaceAll("_", "-");
9
+ const signal = timeout ? AbortSignal.timeout(timeout) : void 0;
9
10
  return e.$fetch(`/__nuxt_island/${component}_${hashId}.json`, {
10
11
  params: {
11
12
  props: JSON.stringify(props)
12
- }
13
+ },
14
+ timeout,
15
+ signal
13
16
  });
14
17
  }
15
18
  export function createNitroRouteRuleMatcher() {
@@ -1,5 +1,5 @@
1
- import { componentNames } from "#og-image-virtual/component-names.mjs";
2
1
  import { createError } from "h3";
2
+ import { componentNames } from "#og-image-virtual/component-names.mjs";
3
3
  const RENDERER_SUFFIXES = ["satori", "browser", "takumi"];
4
4
  const RE_RENDERER_SUFFIX_DOT = /\.?(satori|browser|takumi)$/i;
5
5
  const RE_RENDERER_SUFFIX_PASCAL = /(Satori|Browser|Takumi)$/;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Expand an IPv6 string to its 8 16-bit groups. Accepts `::` shorthand,
3
+ * embedded IPv4 in the trailing 32 bits (`::ffff:1.2.3.4`), and zone IDs
4
+ * (stripped). Returns `null` for any malformed input.
5
+ */
6
+ export declare function expandIPv6(addr: string): number[] | null;
7
+ /**
8
+ * Returns true if the URL points at an internal/private network or otherwise
9
+ * unsafe target. Only `http:` and `https:` are accepted; everything else is
10
+ * blocked outright.
11
+ */
12
+ export declare function isBlockedUrl(url: string): boolean;
13
+ export interface SafeFetchOptions {
14
+ timeout: number;
15
+ headers?: Record<string, string>;
16
+ signal?: AbortSignal;
17
+ }
18
+ /**
19
+ * Fetch a URL with manual redirect handling. Each hop (including the initial
20
+ * URL) is run through `isBlockedUrl` before the request is dispatched, so an
21
+ * allowed origin returning a 30x to an internal IP cannot complete the SSRF.
22
+ *
23
+ * Returns `null` on any failure (block, network error, non-2xx, redirect
24
+ * limit). The caller treats null as a soft failure and falls back to the
25
+ * usual missing-asset behaviour.
26
+ */
27
+ export declare function fetchWithRedirectValidation(initialUrl: string, opts: SafeFetchOptions): Promise<ArrayBuffer | null>;
@@ -0,0 +1,165 @@
1
+ const RE_IPV6_BRACKETS = /^\[|\]$/g;
2
+ const RE_DIGIT_ONLY = /^\d+$/;
3
+ const RE_INT_IP = /^(?:0x[\da-f]+|\d+)$/i;
4
+ const RE_HEX_GROUP = /^[0-9a-f]{1,4}$/i;
5
+ const RE_IPV6_CHARS = /^[0-9a-f:.]+$/i;
6
+ const RE_TRAILING_V4 = /(\d+\.\d+\.\d+\.\d+)$/;
7
+ const MAX_REDIRECTS = 5;
8
+ function isPrivateIPv4(a, b) {
9
+ if (a === 127)
10
+ return true;
11
+ if (a === 10)
12
+ return true;
13
+ if (a === 172 && b >= 16 && b <= 31)
14
+ return true;
15
+ if (a === 192 && b === 168)
16
+ return true;
17
+ if (a === 169 && b === 254)
18
+ return true;
19
+ if (a === 0)
20
+ return true;
21
+ return false;
22
+ }
23
+ export function expandIPv6(addr) {
24
+ const noZone = addr.split("%")[0];
25
+ if (!noZone || !RE_IPV6_CHARS.test(noZone))
26
+ return null;
27
+ let work = noZone;
28
+ const v4Match = work.match(RE_TRAILING_V4);
29
+ if (v4Match) {
30
+ const octets = v4Match[1].split(".").map(Number);
31
+ if (octets.some((o) => !Number.isFinite(o) || o < 0 || o > 255))
32
+ return null;
33
+ const hi = (octets[0] << 8 | octets[1]).toString(16);
34
+ const lo = (octets[2] << 8 | octets[3]).toString(16);
35
+ work = `${work.slice(0, -v4Match[1].length) + hi}:${lo}`;
36
+ }
37
+ const parts = work.split("::");
38
+ if (parts.length > 2)
39
+ return null;
40
+ const head = parts[0] ? parts[0].split(":") : [];
41
+ const tail = parts[1] !== void 0 && parts[1] ? parts[1].split(":") : [];
42
+ if (parts.length === 1) {
43
+ if (head.length !== 8)
44
+ return null;
45
+ } else if (head.length + tail.length > 7) {
46
+ return null;
47
+ }
48
+ const fillCount = parts.length === 2 ? 8 - head.length - tail.length : 0;
49
+ const allGroups = [...head, ...Array.from({ length: fillCount }).fill("0"), ...tail];
50
+ const result = [];
51
+ for (const g of allGroups) {
52
+ if (!RE_HEX_GROUP.test(g))
53
+ return null;
54
+ result.push(Number.parseInt(g, 16));
55
+ }
56
+ return result;
57
+ }
58
+ function isPrivateIPv6(groups) {
59
+ const [g0, g1, g2, g3, g4, g5, g6, g7] = groups;
60
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && g6 === 0 && g7 === 1)
61
+ return true;
62
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && g6 === 0 && g7 === 0)
63
+ return true;
64
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 65535) {
65
+ const a = g6 >> 8 & 255;
66
+ const b = g6 & 255;
67
+ return isPrivateIPv4(a, b);
68
+ }
69
+ if (g0 === 100 && g1 === 65435 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) {
70
+ const a = g6 >> 8 & 255;
71
+ const b = g6 & 255;
72
+ return isPrivateIPv4(a, b);
73
+ }
74
+ if (g0 === 100 && g1 === 65435 && g2 === 1)
75
+ return true;
76
+ if ((g0 & 65024) === 64512)
77
+ return true;
78
+ if ((g0 & 65472) === 65152)
79
+ return true;
80
+ if ((g0 & 65472) === 65216)
81
+ return true;
82
+ if (g0 === 8193 && g1 === 3512)
83
+ return true;
84
+ if ((g0 & 65520) === 16368)
85
+ return true;
86
+ if (g0 === 24320)
87
+ return true;
88
+ if ((g0 & 65280) === 65280)
89
+ return true;
90
+ return false;
91
+ }
92
+ export function isBlockedUrl(url) {
93
+ let parsed;
94
+ try {
95
+ parsed = new URL(url);
96
+ } catch {
97
+ return true;
98
+ }
99
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
100
+ return true;
101
+ const hostname = parsed.hostname.toLowerCase();
102
+ const bare = hostname.replace(RE_IPV6_BRACKETS, "");
103
+ if (bare === "localhost" || bare.endsWith(".localhost"))
104
+ return true;
105
+ const dottedParts = bare.split(".");
106
+ if (dottedParts.length === 4 && dottedParts.every((p) => RE_DIGIT_ONLY.test(p))) {
107
+ const octets = dottedParts.map(Number);
108
+ if (octets.some((o) => o > 255))
109
+ return true;
110
+ return isPrivateIPv4(octets[0], octets[1]);
111
+ }
112
+ if (RE_INT_IP.test(bare)) {
113
+ const num = Number(bare);
114
+ if (Number.isFinite(num) && num >= 0 && num <= 4294967295)
115
+ return isPrivateIPv4(num >>> 24 & 255, num >>> 16 & 255);
116
+ }
117
+ if (bare.includes(":")) {
118
+ const groups = expandIPv6(bare);
119
+ if (groups)
120
+ return isPrivateIPv6(groups);
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+ export async function fetchWithRedirectValidation(initialUrl, opts) {
126
+ const controller = new AbortController();
127
+ const externalAbort = opts.signal;
128
+ const onExternalAbort = () => controller.abort(externalAbort?.reason);
129
+ if (externalAbort) {
130
+ if (externalAbort.aborted)
131
+ controller.abort(externalAbort.reason);
132
+ else
133
+ externalAbort.addEventListener("abort", onExternalAbort, { once: true });
134
+ }
135
+ const timer = setTimeout(() => controller.abort(new Error("timeout")), opts.timeout);
136
+ try {
137
+ let url = initialUrl;
138
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
139
+ if (isBlockedUrl(url))
140
+ return null;
141
+ const res = await fetch(url, {
142
+ redirect: "manual",
143
+ signal: controller.signal,
144
+ headers: opts.headers
145
+ });
146
+ if (res.status >= 300 && res.status < 400) {
147
+ const loc = res.headers.get("location");
148
+ if (!loc)
149
+ return null;
150
+ url = new URL(loc, url).toString();
151
+ continue;
152
+ }
153
+ if (!res.ok)
154
+ return null;
155
+ return await res.arrayBuffer();
156
+ }
157
+ return null;
158
+ } catch {
159
+ return null;
160
+ } finally {
161
+ clearTimeout(timer);
162
+ if (externalAbort)
163
+ externalAbort.removeEventListener("abort", onExternalAbort);
164
+ }
165
+ }
@@ -0,0 +1 @@
1
+ export declare function withTimeout<T>(promise: Promise<T> | T, ms: number, label: string): Promise<T>;
@@ -0,0 +1,12 @@
1
+ export function withTimeout(promise, ms, label) {
2
+ let timer;
3
+ return Promise.race([
4
+ Promise.resolve(promise),
5
+ new Promise((_, reject) => {
6
+ timer = setTimeout(
7
+ () => reject(new Error(`${label} timed out after ${ms}ms`)),
8
+ ms
9
+ );
10
+ })
11
+ ]).finally(() => clearTimeout(timer));
12
+ }
@@ -1,6 +1,6 @@
1
- import { componentNames } from "#og-image-virtual/component-names.mjs";
2
1
  import { useRuntimeConfig } from "nitropack/runtime";
3
2
  import { joinURL } from "ufo";
3
+ import { componentNames } from "#og-image-virtual/component-names.mjs";
4
4
  import { buildOgImageUrl } from "../shared.js";
5
5
  export function getOgImagePath(_pagePath, _options) {
6
6
  const baseURL = useRuntimeConfig().app.baseURL;
@@ -20,9 +20,13 @@ export function getOgImagePath(_pagePath, _options) {
20
20
  }
21
21
  export function useOgImageRuntimeConfig(e) {
22
22
  const c = useRuntimeConfig(e);
23
+ const moduleCfg = c["nuxt-og-image"] || {};
24
+ const overrideSecret = c.ogImage?.secret;
25
+ const security = overrideSecret ? { ...moduleCfg.security || {}, secret: overrideSecret } : moduleCfg.security;
23
26
  return {
24
27
  defaults: {},
25
- ...c["nuxt-og-image"],
28
+ ...moduleCfg,
29
+ security,
26
30
  app: {
27
31
  baseURL: c.app.baseURL
28
32
  }
@@ -5067,7 +5067,7 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
5067
5067
  if (hasNuxtFonts && fontContext) {
5068
5068
  persistFontUrlMapping({ fontContext, buildDir: nuxt.options.buildDir, logger });
5069
5069
  }
5070
- if (!fontProcessingDone && convertedWoff2Files.size === 0 && hasSatoriRenderer() && hasNuxtFonts) {
5070
+ if (!fontProcessingDone && convertedWoff2Files.size === 0 && (hasSatoriRenderer() || hasTakumiRenderer()) && hasNuxtFonts) {
5071
5071
  if (pendingFontRequirements.length > 0)
5072
5072
  await Promise.all(pendingFontRequirements);
5073
5073
  await convertWoff2ToTtf({
@@ -5136,7 +5136,7 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5136
5136
  });
5137
5137
  nuxt.hook("vite:compiled", async () => {
5138
5138
  persistFontUrlMapping({ fontContext, buildDir: nuxt.options.buildDir, logger });
5139
- if (fontProcessingDone || !hasSatoriRenderer())
5139
+ if (fontProcessingDone || !hasSatoriRenderer() && !hasTakumiRenderer())
5140
5140
  return;
5141
5141
  if (pendingFontRequirements.length === 0)
5142
5142
  return;
@@ -5240,6 +5240,11 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5240
5240
  }
5241
5241
  nuxt.hooks.callHook("nuxt-og-image:runtime-config", runtimeConfig);
5242
5242
  nuxt.options.runtimeConfig["nuxt-og-image"] = runtimeConfig;
5243
+ const existingOgImageCfg = nuxt.options.runtimeConfig.ogImage;
5244
+ nuxt.options.runtimeConfig.ogImage = {
5245
+ ...existingOgImageCfg && typeof existingOgImageCfg === "object" ? existingOgImageCfg : {},
5246
+ secret: existingOgImageCfg?.secret || config.security?.secret || process.env.NUXT_OG_IMAGE_SECRET || ""
5247
+ };
5243
5248
  nuxt.options.runtimeConfig.public = {
5244
5249
  ...nuxt.options.runtimeConfig.public,
5245
5250
  "nuxt-og-image": {
@@ -4320,7 +4320,7 @@ const module$1 = kit.defineNuxtModule({
4320
4320
  await onUpgrade(nuxt, options, previousVersion);
4321
4321
  },
4322
4322
  async setup(config, nuxt) {
4323
- const _resolver = kit.createResolver((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/nuxt-og-image.TJuh6pW5.cjs', document.baseURI).href)));
4323
+ const _resolver = kit.createResolver((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('shared/nuxt-og-image.DnwTVrnM.cjs', document.baseURI).href)));
4324
4324
  const fixSharedPath = (p) => {
4325
4325
  if (p.includes("/shared/runtime/"))
4326
4326
  return p.replace("/shared/runtime/", "/runtime/");
@@ -5087,7 +5087,7 @@ export const resolve = (import.meta.dev || import.meta.prerender) ? devResolve :
5087
5087
  if (hasNuxtFonts && fontContext) {
5088
5088
  persistFontUrlMapping({ fontContext, buildDir: nuxt.options.buildDir, logger: logger_js.logger });
5089
5089
  }
5090
- if (!fontProcessingDone && convertedWoff2Files.size === 0 && hasSatoriRenderer() && hasNuxtFonts) {
5090
+ if (!fontProcessingDone && convertedWoff2Files.size === 0 && (hasSatoriRenderer() || hasTakumiRenderer()) && hasNuxtFonts) {
5091
5091
  if (pendingFontRequirements.length > 0)
5092
5092
  await Promise.all(pendingFontRequirements);
5093
5093
  await convertWoff2ToTtf({
@@ -5156,7 +5156,7 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5156
5156
  });
5157
5157
  nuxt.hook("vite:compiled", async () => {
5158
5158
  persistFontUrlMapping({ fontContext, buildDir: nuxt.options.buildDir, logger: logger_js.logger });
5159
- if (fontProcessingDone || !hasSatoriRenderer())
5159
+ if (fontProcessingDone || !hasSatoriRenderer() && !hasTakumiRenderer())
5160
5160
  return;
5161
5161
  if (pendingFontRequirements.length === 0)
5162
5162
  return;
@@ -5260,6 +5260,11 @@ export const rootDir = ${JSON.stringify(nuxt.options.rootDir)}`;
5260
5260
  }
5261
5261
  nuxt.hooks.callHook("nuxt-og-image:runtime-config", runtimeConfig);
5262
5262
  nuxt.options.runtimeConfig["nuxt-og-image"] = runtimeConfig;
5263
+ const existingOgImageCfg = nuxt.options.runtimeConfig.ogImage;
5264
+ nuxt.options.runtimeConfig.ogImage = {
5265
+ ...existingOgImageCfg && typeof existingOgImageCfg === "object" ? existingOgImageCfg : {},
5266
+ secret: existingOgImageCfg?.secret || config.security?.secret || process.env.NUXT_OG_IMAGE_SECRET || ""
5267
+ };
5263
5268
  nuxt.options.runtimeConfig.public = {
5264
5269
  ...nuxt.options.runtimeConfig.public,
5265
5270
  "nuxt-og-image": {