nuxt-og-image 6.7.8 → 6.9.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 (38) hide show
  1. package/dist/chunks/tw4.mjs +2 -2
  2. package/dist/chunks/uno.mjs +4 -4
  3. package/dist/cli.mjs +1 -1
  4. package/dist/devtools/nuxt.config.ts +4 -1
  5. package/dist/module.d.mts +10 -0
  6. package/dist/module.d.ts +10 -0
  7. package/dist/module.json +1 -1
  8. package/dist/module.mjs +3 -3
  9. package/dist/runtime/app/client-utils.js +4 -3
  10. package/dist/runtime/app/utils.js +3 -2
  11. package/dist/runtime/server/nitro.d.ts +9 -0
  12. package/dist/runtime/server/nitro.js +10 -0
  13. package/dist/runtime/server/og-image/bindings/fetch/node.d.ts +2 -0
  14. package/dist/runtime/server/og-image/bindings/fetch/node.js +42 -0
  15. package/dist/runtime/server/og-image/bindings/fetch/web.d.ts +7 -0
  16. package/dist/runtime/server/og-image/bindings/fetch/web.js +3 -0
  17. package/dist/runtime/server/og-image/bindings/font-assets/external-url.d.ts +0 -11
  18. package/dist/runtime/server/og-image/bindings/font-assets/external-url.js +2 -19
  19. package/dist/runtime/server/og-image/bindings/satori/harfbuzz.d.ts +4 -0
  20. package/dist/runtime/server/og-image/bindings/satori/harfbuzz.js +33 -0
  21. package/dist/runtime/server/og-image/browser/screenshot.js +4 -1
  22. package/dist/runtime/server/og-image/context.js +9 -1
  23. package/dist/runtime/server/og-image/font-subsets.d.ts +3 -11
  24. package/dist/runtime/server/og-image/font-subsets.js +50 -27
  25. package/dist/runtime/server/og-image/fonts.d.ts +1 -1
  26. package/dist/runtime/server/routes/resolve.js +2 -2
  27. package/dist/runtime/server/util/cloudflare.js +2 -2
  28. package/dist/runtime/server/util/eventHandlers.js +3 -3
  29. package/dist/runtime/server/util/ssrf.d.ts +31 -0
  30. package/dist/runtime/server/util/ssrf.js +64 -5
  31. package/dist/runtime/server/utils.d.ts +1 -1
  32. package/dist/runtime/server/utils.js +3 -3
  33. package/dist/runtime/shared.d.ts +10 -1
  34. package/dist/runtime/shared.js +2 -2
  35. package/dist/runtime/types.d.ts +5 -1
  36. package/dist/shared/{nuxt-og-image.BMHzg1Vz.mjs → nuxt-og-image.CTD0ZPNd.mjs} +634 -468
  37. package/package.json +70 -66
  38. package/types/virtual.d.ts +26 -0
@@ -10,6 +10,36 @@ export declare function expandIPv6(addr: string): number[] | null;
10
10
  * blocked outright.
11
11
  */
12
12
  export declare function isBlockedUrl(url: string): boolean;
13
+ /** True when a resolved IP address is internal, reserved, or unparseable. */
14
+ export declare function isBlockedAddress(address: string): boolean;
15
+ declare const sameOriginPathBrand: unique symbol;
16
+ /**
17
+ * A path that stays on the current origin when resolved against any base URL.
18
+ * Only `parseSameOriginPath` creates one, so a value of this type is safe to
19
+ * pass to a same-origin fetch or a headless browser navigation.
20
+ */
21
+ export type SameOriginPath = string & {
22
+ readonly [sameOriginPathBrand]: true;
23
+ };
24
+ /**
25
+ * Parse an untrusted path (for example the `_path` URL option) into a
26
+ * same-origin path. Returns `undefined` for anything that could leave the
27
+ * origin: absolute URLs, other schemes, protocol-relative `//host`,
28
+ * backslash variants, control characters, and paths without a leading `/`
29
+ * (which a string concatenation with the origin turns into `origin@host`).
30
+ *
31
+ * GHSA-h5qf-97cw-2h86: an absolute `_path` navigated the browser renderer to
32
+ * an internal host and reflected the response into the image.
33
+ */
34
+ export declare function parseSameOriginPath(input: unknown): SameOriginPath | undefined;
35
+ /**
36
+ * Resolve `path` against `origin` and return the href, but only when the result
37
+ * stays on that origin. Returns null for a missing or unparseable origin, and for
38
+ * anything that resolves elsewhere. The WHATWG parser normalizes leading
39
+ * controls, tabs, and backslashes first, so " //127.0.0.1" resolves cross-origin
40
+ * and is rejected (GHSA-q8hw-4fvp-9rwv).
41
+ */
42
+ export declare function resolveSameOriginUrl(path: string, origin: string | undefined): string | null;
13
43
  export interface SafeFetchOptions {
14
44
  timeout: number;
15
45
  headers?: Record<string, string>;
@@ -35,3 +65,4 @@ export interface SafeFetchOptions {
35
65
  * usual missing-asset behaviour.
36
66
  */
37
67
  export declare function fetchWithRedirectValidation(initialUrl: string, opts: SafeFetchOptions): Promise<ArrayBuffer | null>;
68
+ export {};
@@ -1,4 +1,6 @@
1
+ import { guardedFetch } from "#og-image/bindings/fetch";
1
2
  const RE_IPV6_BRACKETS = /^\[|\]$/g;
3
+ const RE_TRAILING_DOTS = /\.+$/;
2
4
  const RE_DIGIT_ONLY = /^\d+$/;
3
5
  const RE_INT_IP = /^(?:0x[\da-f]+|\d+)$/i;
4
6
  const RE_HEX_GROUP = /^[0-9a-f]{1,4}$/i;
@@ -18,6 +20,12 @@ function isPrivateIPv4(a, b) {
18
20
  return true;
19
21
  if (a === 0)
20
22
  return true;
23
+ if (a === 100 && b >= 64 && b <= 127)
24
+ return true;
25
+ if (a === 198 && (b === 18 || b === 19))
26
+ return true;
27
+ if (a >= 224)
28
+ return true;
21
29
  return false;
22
30
  }
23
31
  export function expandIPv6(addr) {
@@ -88,6 +96,10 @@ function isPrivateIPv6(groups) {
88
96
  }
89
97
  if (g0 === 100 && g1 === 65435 && g2 === 1)
90
98
  return true;
99
+ if (g0 === 256 && g1 === 0 && g2 === 0 && g3 === 0)
100
+ return true;
101
+ if (g0 === 8193 && g1 === 0)
102
+ return true;
91
103
  if ((g0 & 65024) === 64512)
92
104
  return true;
93
105
  if ((g0 & 65472) === 65152)
@@ -114,7 +126,7 @@ export function isBlockedUrl(url) {
114
126
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
115
127
  return true;
116
128
  const hostname = parsed.hostname.toLowerCase();
117
- const bare = hostname.replace(RE_IPV6_BRACKETS, "");
129
+ const bare = hostname.replace(RE_IPV6_BRACKETS, "").replace(RE_TRAILING_DOTS, "");
118
130
  if (bare === "localhost" || bare.endsWith(".localhost"))
119
131
  return true;
120
132
  const dottedParts = bare.split(".");
@@ -137,6 +149,50 @@ export function isBlockedUrl(url) {
137
149
  }
138
150
  return false;
139
151
  }
152
+ export function isBlockedAddress(address) {
153
+ return isBlockedUrl(`http://${address.includes(":") ? `[${address}]` : address}/`);
154
+ }
155
+ const PROBE_ORIGIN = "http://og-image.invalid";
156
+ function hasUnsafePathChar(input) {
157
+ for (let i = 0; i < input.length; i++) {
158
+ const code = input.charCodeAt(i);
159
+ if (code <= 31 || code === 127 || code === 92)
160
+ return true;
161
+ }
162
+ return false;
163
+ }
164
+ export function parseSameOriginPath(input) {
165
+ if (typeof input !== "string" || input[0] !== "/" || input[1] === "/")
166
+ return void 0;
167
+ if (hasUnsafePathChar(input))
168
+ return void 0;
169
+ let resolved;
170
+ try {
171
+ resolved = new URL(input, PROBE_ORIGIN);
172
+ } catch {
173
+ return void 0;
174
+ }
175
+ if (resolved.origin !== PROBE_ORIGIN)
176
+ return void 0;
177
+ return input;
178
+ }
179
+ export function resolveSameOriginUrl(path, origin) {
180
+ if (!origin)
181
+ return null;
182
+ let base;
183
+ try {
184
+ base = new URL(origin).origin;
185
+ } catch {
186
+ return null;
187
+ }
188
+ let target;
189
+ try {
190
+ target = new URL(path, base);
191
+ } catch {
192
+ return null;
193
+ }
194
+ return target.origin === base ? target.href : null;
195
+ }
140
196
  export async function fetchWithRedirectValidation(initialUrl, opts) {
141
197
  const controller = new AbortController();
142
198
  const externalAbort = opts.signal;
@@ -161,20 +217,23 @@ export async function fetchWithRedirectValidation(initialUrl, opts) {
161
217
  }
162
218
  if (!isTrusted && isBlockedUrl(url))
163
219
  return null;
164
- const res = await fetch(url, {
165
- redirect: "manual",
220
+ const res = await guardedFetch(url, {
166
221
  signal: controller.signal,
167
- headers: opts.headers
222
+ headers: opts.headers,
223
+ isBlockedAddress: isTrusted ? void 0 : isBlockedAddress
168
224
  });
169
225
  if (res.status >= 300 && res.status < 400) {
226
+ await res.body?.cancel();
170
227
  const loc = res.headers.get("location");
171
228
  if (!loc)
172
229
  return null;
173
230
  url = new URL(loc, url).toString();
174
231
  continue;
175
232
  }
176
- if (!res.ok)
233
+ if (!res.ok) {
234
+ await res.body?.cancel();
177
235
  return null;
236
+ }
178
237
  return await res.arrayBuffer();
179
238
  }
180
239
  return null;
@@ -4,5 +4,5 @@ export interface GetOgImagePathResult {
4
4
  path: string;
5
5
  hash?: string;
6
6
  }
7
- export declare function getOgImagePath(_pagePath: string, _options?: Partial<OgImageOptionsInternal>): GetOgImagePathResult;
7
+ export declare function getOgImagePath(event: H3Event, _pagePath: string, _options?: Partial<OgImageOptionsInternal>): GetOgImagePathResult;
8
8
  export declare function useOgImageRuntimeConfig(e?: H3Event): OgImageRuntimeConfig;
@@ -3,9 +3,9 @@ import { useRuntimeConfig } from "#nuxtseo/nitro";
3
3
  import { componentNames } from "#og-image-virtual/component-names.mjs";
4
4
  import { buildOgImageUrl } from "../shared.js";
5
5
  import { getCloudflareEnv } from "./util/cloudflare.js";
6
- export function getOgImagePath(_pagePath, _options) {
7
- const baseURL = useRuntimeConfig().app.baseURL;
8
- const { defaults, security } = useOgImageRuntimeConfig();
6
+ export function getOgImagePath(event, _pagePath, _options) {
7
+ const baseURL = useRuntimeConfig(event).app.baseURL;
8
+ const { defaults, security } = useOgImageRuntimeConfig(event);
9
9
  const extension = _options?.extension || defaults.extension;
10
10
  const isStatic = import.meta.prerender && !(security?.secret && security?.strict);
11
11
  const options = { ..._options, _path: _pagePath };
@@ -2,7 +2,16 @@ import type { ResolvableMeta } from '@unhead/vue';
2
2
  import type { OgImageOptions, OgImagePrebuilt } from './types.js';
3
3
  export { extractSocialPreviewTags, toBase64Image } from './pure.js';
4
4
  export { buildOgImageUrl, decodeOgImageParams, encodeOgImageParams, extractEncodedSegment, hashOgImageOptions, parseOgImageUrl, signEncodedParams, verifyOgImageSignature } from './shared/urlEncoding.js';
5
- export declare function generateMeta(url: OgImagePrebuilt['url'] | string, resolvedOptions: OgImageOptions | OgImagePrebuilt): ResolvableMeta[];
5
+ export interface GenerateMetaOptions {
6
+ /**
7
+ * Emit Twitter card meta tags (`twitter:card`, `twitter:image`, etc) alongside
8
+ * the Open Graph image tags.
9
+ *
10
+ * @default true
11
+ */
12
+ includeTwitter?: boolean;
13
+ }
14
+ export declare function generateMeta(url: OgImagePrebuilt['url'] | string, resolvedOptions: OgImageOptions | OgImagePrebuilt, metaOptions?: GenerateMetaOptions): ResolvableMeta[];
6
15
  export declare function isInternalRoute(path: string): boolean;
7
16
  export declare function separateProps(options: OgImageOptions | undefined, ignoreKeys?: string[]): OgImageOptions;
8
17
  /**
@@ -3,10 +3,10 @@ import { toValue } from "vue";
3
3
  export { extractSocialPreviewTags, toBase64Image } from "./pure.js";
4
4
  export { buildOgImageUrl, decodeOgImageParams, encodeOgImageParams, extractEncodedSegment, hashOgImageOptions, parseOgImageUrl, signEncodedParams, verifyOgImageSignature } from "./shared/urlEncoding.js";
5
5
  const RE_KEBAB_CASE = /-([a-z])/g;
6
- export function generateMeta(url, resolvedOptions) {
6
+ export function generateMeta(url, resolvedOptions, metaOptions = {}) {
7
7
  const key = resolvedOptions.key || "og";
8
8
  const isTwitterOnly = key === "twitter";
9
- const includeTwitter = key === "og" || key === "twitter";
9
+ const includeTwitter = (key === "og" || key === "twitter") && metaOptions.includeTwitter !== false;
10
10
  const meta = [];
11
11
  if (includeTwitter) {
12
12
  meta.push({ name: "twitter:card", content: "summary_large_image" });
@@ -4,12 +4,13 @@ import type { SatoriOptions } from 'satori';
4
4
  import type { JpegOptions, SharpOptions } from 'sharp';
5
5
  import type { MaybeRefOrGetter, Ref } from 'vue';
6
6
  import type { H3Error, H3Event } from '#nuxtseo/h3';
7
+ import type { SameOriginPath } from './server/util/ssrf.js';
7
8
  import type { Timings } from './server/util/timings.js';
8
9
  export interface OgImageRenderEventContext {
9
10
  e: H3Event;
10
11
  extension: 'png' | 'jpeg' | 'jpg' | 'webp' | 'svg' | 'html' | 'json';
11
12
  key: string;
12
- basePath: string;
13
+ basePath: SameOriginPath;
13
14
  renderer: Renderer;
14
15
  options: OgImageOptionsInternal;
15
16
  isDevToolsContextRequest: boolean;
@@ -37,6 +38,8 @@ export interface OgImageRuntimeConfig {
37
38
  publicStoragePath: string;
38
39
  defaults: Omit<OgImageOptions, 'component' | 'renderer' | 'props' | 'url' | 'html' | 'key' | 'cacheKey' | '_query' | '_hash' | 'socialPreview'>;
39
40
  debug: boolean;
41
+ /** Whether twitter:* meta tags are emitted alongside og:image tags */
42
+ includeTwitter: boolean;
40
43
  baseCacheKey: string;
41
44
  hasNuxtIcon: boolean;
42
45
  hasNuxtContent?: boolean;
@@ -239,6 +242,7 @@ export interface RuntimeCompatibilitySchema {
239
242
  takumi: 'node' | 'node-dev' | 'wasm' | false;
240
243
  sharp: 'node' | false;
241
244
  emoji?: 'local' | 'fetch';
245
+ fetch?: 'node' | 'web';
242
246
  wasm?: any;
243
247
  }
244
248
  export interface RuntimeCompatibilityMeta {