nuxt-og-image 6.5.3 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/chunks/tw4.cjs +7 -7
- package/dist/chunks/tw4.mjs +7 -7
- package/dist/chunks/uno.cjs +6 -6
- package/dist/chunks/uno.mjs +6 -6
- package/dist/cli.cjs +12 -5
- package/dist/cli.mjs +10 -3
- package/dist/devtools/components/og-image/AddComponentDialog.vue +85 -0
- package/dist/devtools/components/og-image/BlueskyCardRenderer.vue +134 -0
- package/dist/devtools/components/og-image/CreateOgImageDialog.vue +56 -0
- package/dist/devtools/components/og-image/DiscordCardRenderer.vue +125 -0
- package/dist/devtools/components/og-image/FacebookCardRenderer.vue +128 -0
- package/dist/devtools/components/og-image/IFrameLoader.vue +93 -0
- package/dist/devtools/components/og-image/ImageLoader.vue +197 -0
- package/dist/devtools/components/og-image/LinkedInCardRenderer.vue +100 -0
- package/dist/devtools/components/og-image/RendererSelectModal.vue +356 -0
- package/dist/devtools/components/og-image/SlackCardRenderer.vue +140 -0
- package/dist/devtools/components/og-image/TemplateComponentPreview.vue +186 -0
- package/dist/devtools/components/og-image/TwitterCardRenderer.vue +170 -0
- package/dist/devtools/components/og-image/WhatsAppRenderer.vue +294 -0
- package/dist/devtools/lib/og-image/keys.ts +8 -0
- package/dist/devtools/lib/og-image/og-image.ts +536 -0
- package/dist/devtools/lib/og-image/renderer-select.ts +3 -0
- package/dist/devtools/lib/og-image/rpc-types.ts +2 -0
- package/dist/devtools/lib/og-image/rpc.ts +73 -0
- package/dist/devtools/lib/og-image/runtime-types.ts +10 -0
- package/dist/devtools/lib/og-image/shared/urlEncoding.ts +502 -0
- package/dist/devtools/lib/og-image/shared.ts +30 -0
- package/dist/devtools/lib/og-image/templates.ts +9 -0
- package/dist/devtools/lib/og-image/types.ts +38 -0
- package/dist/devtools/lib/og-image/util/logic.ts +21 -0
- package/dist/devtools/nuxt.config.ts +6 -0
- package/dist/devtools/pages/og-image/debug.vue +32 -0
- package/dist/devtools/pages/og-image/docs.vue +3 -0
- package/dist/devtools/pages/og-image/index.vue +1682 -0
- package/dist/devtools/pages/og-image/templates.vue +150 -0
- package/dist/devtools/pages/og-image.vue +184 -0
- package/dist/module.cjs +5 -5
- package/dist/module.d.cts +5 -3
- package/dist/module.d.mts +5 -3
- package/dist/module.d.ts +5 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -5
- package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.d.ts +1 -1
- package/dist/runtime/server/og-image/bindings/font-assets/cloudflare.js +6 -1
- package/dist/runtime/server/og-image/bindings/font-assets/dev-prerender.js +13 -6
- package/dist/runtime/server/og-image/bindings/font-assets/external-url.d.ts +36 -0
- package/dist/runtime/server/og-image/bindings/font-assets/external-url.js +47 -0
- package/dist/runtime/server/og-image/bindings/font-assets/node.d.ts +1 -1
- package/dist/runtime/server/og-image/bindings/font-assets/node.js +14 -7
- package/dist/runtime/server/og-image/bindings/takumi/node-dev.js +7 -1
- package/dist/runtime/server/og-image/core/plugins/imageSrc.js +2 -2
- package/dist/runtime/server/og-image/fonts.d.ts +4 -0
- package/dist/runtime/server/og-image/fonts.js +21 -5
- package/dist/runtime/server/og-image/satori/instances.d.ts +1 -2
- package/dist/runtime/server/og-image/takumi/renderer.js +61 -19
- package/dist/runtime/server/util/ssrf.d.ts +10 -0
- package/dist/runtime/server/util/ssrf.js +9 -1
- package/dist/runtime/types.d.ts +13 -0
- package/dist/shared/{nuxt-og-image.DdbTs-xp.mjs → nuxt-og-image.CPSxZhVO.mjs} +122 -496
- package/dist/shared/nuxt-og-image.DM30kcDz.mjs +660 -0
- package/dist/shared/nuxt-og-image.RI9DgjgV.cjs +689 -0
- package/dist/shared/{nuxt-og-image.CfTPCtaS.cjs → nuxt-og-image.yDJBWXBt.cjs} +181 -555
- package/package.json +48 -45
- package/dist/devtools/200.html +0 -1
- package/dist/devtools/404.html +0 -1
- package/dist/devtools/_fonts/4ppnHhMi-pBsWSPo7mY0avYxlDoAg1N3PTzCwXLZ5rA-d9oibkGnTd1JL3tc_xnaVgBLYmOB8kjrK2cvZaqwj9s.woff2 +0 -0
- package/dist/devtools/_fonts/PV2hrQG6wq5BlIPDjdL1IcOflycaghyt5MHzlBqZtlo-lb_WexLz3VZqfTN0oi554iBH5tT2j2UFEV-XErCAS3E.woff2 +0 -0
- package/dist/devtools/_fonts/VE4cDVCv5MxbFM7ZLoLCGbIpNd71zhp7MDI9lmN5Y7I-xZyDYCUVrd6LV8eVGF3Um3UZjBFuUtDGtvdyTBBRYBo.woff2 +0 -0
- package/dist/devtools/_fonts/fVoGbnMbBFd5L9BBp9fUPavUSkZ_EmsQNSyadkT-108-U4T0khaeLQSIhtt9eVvaCEKJjtWJ4ioRJOf8hvqkWY0.woff2 +0 -0
- package/dist/devtools/_fonts/lQAxeCEs1R0Lw-H9XRU1RlOARQN8J6npRsPjyEDMe5s-_DUSLEkO3tKTuun_gSnDLoQPVEnpOnyqZMOw0ByZ6PA.woff2 +0 -0
- package/dist/devtools/_fonts/lntlqNHKLV2n82yTwMde70QqOjcfLE2XJ5oKZ3vRPWc-z6TxpIZQdWXztWLr9_OFWqt_WJJoeGtuK_-XQMZGQwE.woff2 +0 -0
- package/dist/devtools/_nuxt/BZ_CG9ll.js +0 -30
- package/dist/devtools/_nuxt/BdMalyEU.js +0 -4
- package/dist/devtools/_nuxt/BhkGaWEe.js +0 -3
- package/dist/devtools/_nuxt/Bm4Q-nTW.js +0 -3
- package/dist/devtools/_nuxt/BsRKQH5X.js +0 -1
- package/dist/devtools/_nuxt/C5797Ieg.js +0 -1
- package/dist/devtools/_nuxt/CBcJg2GG.js +0 -2
- package/dist/devtools/_nuxt/CDaXKRRu.js +0 -1
- package/dist/devtools/_nuxt/CM-t-6ZT.js +0 -1
- package/dist/devtools/_nuxt/CP0tQR2M.js +0 -1
- package/dist/devtools/_nuxt/CXAM78_A.js +0 -1
- package/dist/devtools/_nuxt/CYDI7SLa.js +0 -1
- package/dist/devtools/_nuxt/CkbDfutK.js +0 -152
- package/dist/devtools/_nuxt/Co8Tk_C2.js +0 -1
- package/dist/devtools/_nuxt/CqjbkMN9.js +0 -3
- package/dist/devtools/_nuxt/D49I50CU.js +0 -1
- package/dist/devtools/_nuxt/DevtoolsSection.DkT2bJJh.css +0 -1
- package/dist/devtools/_nuxt/DevtoolsSnippet.zV_SDojn.css +0 -1
- package/dist/devtools/_nuxt/HWyD49jH.js +0 -1
- package/dist/devtools/_nuxt/IFrameLoader.k_861Nnq.css +0 -1
- package/dist/devtools/_nuxt/PONEy9N-.js +0 -1
- package/dist/devtools/_nuxt/builds/latest.json +0 -1
- package/dist/devtools/_nuxt/builds/meta/0f3dbc15-8308-4956-81b9-5507cac7018c.json +0 -1
- package/dist/devtools/_nuxt/entry.D7u2asd2.css +0 -2
- package/dist/devtools/_nuxt/fira-code.Bc8wnsZt.woff2 +0 -0
- package/dist/devtools/_nuxt/hubot-sans.DLGyhQVu.woff2 +0 -0
- package/dist/devtools/_nuxt/pO9PmtPW.js +0 -6
- package/dist/devtools/_nuxt/pages.0Jk10xlb.css +0 -1
- package/dist/devtools/_nuxt/renderer-select.CN5IWe60.css +0 -1
- package/dist/devtools/_nuxt/templates.BByln3BG.css +0 -1
- package/dist/devtools/debug/index.html +0 -1
- package/dist/devtools/docs/index.html +0 -1
- package/dist/devtools/index.html +0 -1
- package/dist/devtools/templates/index.html +0 -1
- package/dist/shared/nuxt-og-image.CJa2KCie.mjs +0 -189
- package/dist/shared/nuxt-og-image.CMYbz66P.cjs +0 -193
|
@@ -17,7 +17,10 @@ parentPort.on('message', async ({ id, type, newFonts, nodes, options }) => {
|
|
|
17
17
|
const fontWarnings = []
|
|
18
18
|
for (const font of (newFonts || [])) {
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
const registerFont = renderer.registerFont || renderer.loadFont
|
|
21
|
+
if (typeof registerFont !== 'function')
|
|
22
|
+
throw new Error('renderer does not expose loadFont/registerFont')
|
|
23
|
+
await registerFont.call(renderer, {
|
|
21
24
|
name: font.name,
|
|
22
25
|
data: font.data,
|
|
23
26
|
weight: font.weight,
|
|
@@ -127,6 +130,9 @@ class RendererWorkerProxy {
|
|
|
127
130
|
this.allFonts.push(font);
|
|
128
131
|
this.pendingFonts.push(font);
|
|
129
132
|
}
|
|
133
|
+
registerFont(font) {
|
|
134
|
+
this.loadFont(font);
|
|
135
|
+
}
|
|
130
136
|
render(nodes, options) {
|
|
131
137
|
ensureWorker();
|
|
132
138
|
let fontsToSend;
|
|
@@ -120,8 +120,8 @@ export default defineTransformer([
|
|
|
120
120
|
let src = node.props.src;
|
|
121
121
|
if (src.startsWith("data:"))
|
|
122
122
|
return;
|
|
123
|
-
if (src.endsWith(".webp")) {
|
|
124
|
-
logger.warn("Using WebP images with Satori is not supported. Please consider switching image format or use the
|
|
123
|
+
if (ctx.renderer.name === "satori" && src.endsWith(".webp")) {
|
|
124
|
+
logger.warn("Using WebP images with Satori is not supported. Please consider switching image format or use the takumi or browser renderer.", src);
|
|
125
125
|
}
|
|
126
126
|
const isRelative = src.startsWith("/");
|
|
127
127
|
if (!isRelative)
|
|
@@ -42,6 +42,10 @@ export declare function loadAllFonts(event: H3Event, options: LoadFontsOptions):
|
|
|
42
42
|
/**
|
|
43
43
|
* Load additional fonts specified via defineOgImage({ fonts: [...] }).
|
|
44
44
|
* Only object format ({ name, weight, path }) with a path is supported.
|
|
45
|
+
*
|
|
46
|
+
* Field values arrive from the attacker-controlled `fonts` URL param, so each is
|
|
47
|
+
* parsed at this boundary (require string name/path, coerce weight) and the
|
|
48
|
+
* array is bounded before any font is fetched.
|
|
45
49
|
*/
|
|
46
50
|
export declare function loadDefinedFonts(event: OgImageRenderEventContext, fontDefs: any[]): Promise<RuntimeFontConfig[]>;
|
|
47
51
|
export interface LoadFontsForRendererOptions extends LoadFontsOptions {
|
|
@@ -172,15 +172,31 @@ export async function loadAllFonts(event, options) {
|
|
|
172
172
|
}
|
|
173
173
|
return loaded;
|
|
174
174
|
}
|
|
175
|
+
const MAX_DEFINED_FONTS = 10;
|
|
176
|
+
const MAX_FONT_PATH_LENGTH = 2048;
|
|
177
|
+
function isDataUri(path) {
|
|
178
|
+
return path.trimStart().toLowerCase().startsWith("data:");
|
|
179
|
+
}
|
|
180
|
+
function normalizeFontWeight(weight) {
|
|
181
|
+
const n = typeof weight === "number" ? weight : typeof weight === "string" ? Number(weight) : Number.NaN;
|
|
182
|
+
if (!Number.isFinite(n))
|
|
183
|
+
return 400;
|
|
184
|
+
return Math.min(Math.max(Math.round(n), 1), 1e3);
|
|
185
|
+
}
|
|
175
186
|
export async function loadDefinedFonts(event, fontDefs) {
|
|
176
187
|
const results = [];
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
const defs = Array.isArray(fontDefs) ? fontDefs : [];
|
|
189
|
+
if (import.meta.dev && defs.length > MAX_DEFINED_FONTS)
|
|
190
|
+
logger.warn(`defineOgImage fonts capped at ${MAX_DEFINED_FONTS}; ${defs.length - MAX_DEFINED_FONTS} ignored.`);
|
|
191
|
+
for (const def of defs.slice(0, MAX_DEFINED_FONTS)) {
|
|
192
|
+
if (!def || typeof def !== "object")
|
|
193
|
+
continue;
|
|
194
|
+
const name = typeof def.name === "string" ? def.name.trim() : "";
|
|
195
|
+
const path = typeof def.path === "string" ? def.path : "";
|
|
196
|
+
if (!name || !path || path.length > MAX_FONT_PATH_LENGTH && !isDataUri(path))
|
|
179
197
|
continue;
|
|
180
|
-
const
|
|
181
|
-
const weight = def.weight || 400;
|
|
198
|
+
const weight = normalizeFontWeight(def.weight);
|
|
182
199
|
const style = def.style === "italic" ? "italic" : "normal";
|
|
183
|
-
const path = def.path;
|
|
184
200
|
const fontConfig = { family: name, weight, style, src: path, localPath: path };
|
|
185
201
|
const data = await resolve(event.e, fontConfig).catch(() => null);
|
|
186
202
|
if (data) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type _satori from 'satori';
|
|
2
|
-
import type _sharp from 'sharp';
|
|
3
2
|
export declare function getResvg(): Promise<any>;
|
|
4
3
|
export declare function getSatori(): Promise<typeof _satori>;
|
|
5
|
-
export declare function getSharp(): Promise<
|
|
4
|
+
export declare function getSharp(): Promise<import("sharp").SharpConstructor>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defu } from "defu";
|
|
2
2
|
import { withBase } from "ufo";
|
|
3
|
+
import compatibility from "#og-image/compatibility";
|
|
3
4
|
import { logger } from "../../../logger.js";
|
|
4
5
|
import { fetchLocalAsset } from "../../util/fetchLocalAsset.js";
|
|
5
6
|
import { getFetchTimeout } from "../../util/fetchTimeout.js";
|
|
@@ -98,25 +99,52 @@ function dedupeFontsByBinary(fonts) {
|
|
|
98
99
|
}
|
|
99
100
|
return result;
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
function toUint8Array(data) {
|
|
103
|
+
if (data instanceof Uint8Array)
|
|
104
|
+
return data;
|
|
105
|
+
if (data instanceof ArrayBuffer)
|
|
106
|
+
return new Uint8Array(data);
|
|
107
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
108
|
+
}
|
|
109
|
+
function createFontDescriptor(entry, includeKey = false) {
|
|
110
|
+
return {
|
|
111
|
+
...includeKey ? { key: entry.binaryKey } : {},
|
|
112
|
+
name: entry.family,
|
|
113
|
+
data: toUint8Array(entry.data),
|
|
114
|
+
...entry.weight !== void 0 ? { weight: entry.weight } : {},
|
|
115
|
+
...entry.style ? { style: entry.style } : {}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function addLoadedFamilies(state, entry, registeredFamilies) {
|
|
119
|
+
if (Array.isArray(registeredFamilies) && registeredFamilies.length) {
|
|
120
|
+
for (const family of registeredFamilies) {
|
|
121
|
+
const name = family?.name;
|
|
122
|
+
if (typeof name === "string")
|
|
123
|
+
state.loadedFamilies.add(name);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
state.loadedFamilies.add(entry.family);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function loadFontsIntoRenderer(state, entries) {
|
|
130
|
+
for (const entry of entries) {
|
|
103
131
|
if (state.loadedFontKeys.has(entry.binaryKey))
|
|
104
132
|
continue;
|
|
105
|
-
const fontData = entry.data instanceof ArrayBuffer ? new Uint8Array(entry.data) : Uint8Array.from(entry.data);
|
|
106
133
|
try {
|
|
107
|
-
await state.renderer.loadFont(
|
|
108
|
-
|
|
109
|
-
data: fontData,
|
|
110
|
-
...entry.weight !== void 0 ? { weight: entry.weight } : {},
|
|
111
|
-
style: entry.style
|
|
112
|
-
});
|
|
113
|
-
state.loadedFamilies.add(entry.family);
|
|
134
|
+
const registeredFamilies = await state.renderer.loadFont(createFontDescriptor(entry));
|
|
135
|
+
addLoadedFamilies(state, entry, registeredFamilies);
|
|
114
136
|
} catch (err) {
|
|
115
|
-
logger.warn(`Failed to load font "${entry.family}"
|
|
137
|
+
logger.warn(`Failed to load font "${entry.family}" with takumi renderer: ${err.message}`);
|
|
116
138
|
}
|
|
117
139
|
state.loadedFontKeys.add(entry.binaryKey);
|
|
118
140
|
}
|
|
119
141
|
}
|
|
142
|
+
function normalizeImageSources(images) {
|
|
143
|
+
return (images || []).map((image) => ({
|
|
144
|
+
src: image.src,
|
|
145
|
+
data: image.data instanceof Uint8Array ? image.data : new Uint8Array(image.data)
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
120
148
|
function lookupFontFamily(family, loadedFamilies) {
|
|
121
149
|
if (loadedFamilies.has(family))
|
|
122
150
|
return family;
|
|
@@ -155,10 +183,12 @@ function rewriteFontFamilies(node, loadedFamilies, subsetChains) {
|
|
|
155
183
|
}
|
|
156
184
|
async function createImage(event, format) {
|
|
157
185
|
const { options, timings } = event;
|
|
186
|
+
const isTakumiV2 = compatibility.takumiVersion === 2;
|
|
158
187
|
const { fontFamilyOverride, defaultFont } = getDefaultFontFamily(options);
|
|
159
188
|
const nodes = await createTakumiNodes(event);
|
|
160
189
|
const codepoints = extractCodepoints(nodes);
|
|
161
190
|
const fonts = await timings.measure("font-load", () => loadFontsForRenderer(event, { supportedFormats: /* @__PURE__ */ new Set(["ttf", "woff2"]), preferStatic: true, component: options.component, fontFamilyOverride: fontFamilyOverride || defaultFont, codepoints }));
|
|
191
|
+
const fontEntries = dedupeFontsByBinary(fonts);
|
|
162
192
|
const hookTimeout = event.runtimeConfig.security?.renderTimeout ?? 15e3;
|
|
163
193
|
await withTimeout(
|
|
164
194
|
event._nitro.hooks.callHook("nuxt-og-image:takumi:nodes", nodes, event),
|
|
@@ -170,7 +200,12 @@ async function createImage(event, format) {
|
|
|
170
200
|
const extractResourceUrls = await timings.measure("takumi-extract-init", () => getExtractResourceUrls());
|
|
171
201
|
const resourceUrls = await extractResourceUrls(nodes);
|
|
172
202
|
const baseURL = event.runtimeConfig.app.baseURL;
|
|
173
|
-
const fetchedResources =
|
|
203
|
+
const { images: optionImages, fetchedResources: optionFetchedResources, persistentImages, ...takumiOptions } = options.takumi || {};
|
|
204
|
+
const images = [
|
|
205
|
+
...normalizeImageSources(optionImages),
|
|
206
|
+
...normalizeImageSources(optionFetchedResources),
|
|
207
|
+
...normalizeImageSources(persistentImages)
|
|
208
|
+
];
|
|
174
209
|
if (resourceUrls.length) {
|
|
175
210
|
const fetchTimeout = getFetchTimeout(event.runtimeConfig);
|
|
176
211
|
const headers = { "x-nuxt-og-image": "1" };
|
|
@@ -200,17 +235,17 @@ async function createImage(event, format) {
|
|
|
200
235
|
}) ?? void 0;
|
|
201
236
|
}
|
|
202
237
|
if (data)
|
|
203
|
-
|
|
238
|
+
images.push({ src, data: new Uint8Array(data) });
|
|
204
239
|
})));
|
|
205
240
|
}
|
|
206
241
|
const maxDpr = event.runtimeConfig.security?.maxDpr || 2;
|
|
207
242
|
const maxDim = event.runtimeConfig.security?.maxDimension || 2048;
|
|
208
243
|
const dpr = Math.min(Math.max(1, options.takumi?.devicePixelRatio ?? 1), maxDpr);
|
|
209
|
-
const renderOptions = defu(
|
|
244
|
+
const renderOptions = defu(takumiOptions, {
|
|
210
245
|
width: Math.min(Number(options.width) * dpr, maxDim),
|
|
211
246
|
height: Math.min(Number(options.height) * dpr, maxDim),
|
|
212
247
|
format,
|
|
213
|
-
fetchedResources,
|
|
248
|
+
...isTakumiV2 ? { images } : { fetchedResources: images },
|
|
214
249
|
devicePixelRatio: dpr
|
|
215
250
|
});
|
|
216
251
|
const lockTimeout = event.runtimeConfig.security?.renderTimeout ?? 15e3;
|
|
@@ -218,19 +253,26 @@ async function createImage(event, format) {
|
|
|
218
253
|
return await withTakumiLock(state, lockTimeout, () => {
|
|
219
254
|
endLockWait();
|
|
220
255
|
return timings.measure("render-takumi", async () => {
|
|
221
|
-
|
|
256
|
+
let loadedFamilies = state.loadedFamilies;
|
|
257
|
+
if (isTakumiV2) {
|
|
258
|
+
loadedFamilies = new Set(fontEntries.map((entry) => entry.family));
|
|
259
|
+
renderOptions.fonts = fontEntries.map((entry) => createFontDescriptor(entry, true));
|
|
260
|
+
renderOptions.fontFamilies = [...loadedFamilies];
|
|
261
|
+
} else {
|
|
262
|
+
await loadFontsIntoRenderer(state, fontEntries);
|
|
263
|
+
}
|
|
222
264
|
const rootStyle = nodes.style ?? {};
|
|
223
265
|
if (fontFamilyOverride) {
|
|
224
266
|
const chain = subsetChains.get(fontFamilyOverride);
|
|
225
267
|
if (chain) {
|
|
226
268
|
rootStyle.fontFamily = chain.map((f) => `"${f}"`).join(", ");
|
|
227
|
-
} else if (
|
|
269
|
+
} else if (loadedFamilies.has(fontFamilyOverride)) {
|
|
228
270
|
rootStyle.fontFamily = fontFamilyOverride;
|
|
229
271
|
}
|
|
230
272
|
}
|
|
231
273
|
nodes.style = rootStyle;
|
|
232
|
-
rewriteFontFamilies(nodes,
|
|
233
|
-
return state.renderer.render(nodes, renderOptions);
|
|
274
|
+
rewriteFontFamilies(nodes, loadedFamilies, subsetChains);
|
|
275
|
+
return await state.renderer.render(nodes, renderOptions);
|
|
234
276
|
});
|
|
235
277
|
}, endLockWait);
|
|
236
278
|
}
|
|
@@ -14,11 +14,21 @@ export interface SafeFetchOptions {
|
|
|
14
14
|
timeout: number;
|
|
15
15
|
headers?: Record<string, string>;
|
|
16
16
|
signal?: AbortSignal;
|
|
17
|
+
/**
|
|
18
|
+
* A host (hostname + optional port) that is exempt from `isBlockedUrl`. Used
|
|
19
|
+
* for same-origin asset fetches whose own origin may legitimately be loopback
|
|
20
|
+
* (dev) or otherwise "private" — the initial host is trusted, but any redirect
|
|
21
|
+
* that leaves it is still re-validated, closing the open-redirect SSRF where
|
|
22
|
+
* the app's own origin 30x's to an internal target.
|
|
23
|
+
*/
|
|
24
|
+
trustedHost?: string;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* Fetch a URL with manual redirect handling. Each hop (including the initial
|
|
20
28
|
* URL) is run through `isBlockedUrl` before the request is dispatched, so an
|
|
21
29
|
* allowed origin returning a 30x to an internal IP cannot complete the SSRF.
|
|
30
|
+
* A hop whose host matches `opts.trustedHost` skips the block check (see
|
|
31
|
+
* SafeFetchOptions.trustedHost).
|
|
22
32
|
*
|
|
23
33
|
* Returns `null` on any failure (block, network error, non-2xx, redirect
|
|
24
34
|
* limit). The caller treats null as a soft failure and falls back to the
|
|
@@ -151,7 +151,15 @@ export async function fetchWithRedirectValidation(initialUrl, opts) {
|
|
|
151
151
|
try {
|
|
152
152
|
let url = initialUrl;
|
|
153
153
|
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
154
|
-
|
|
154
|
+
let isTrusted = false;
|
|
155
|
+
if (opts.trustedHost) {
|
|
156
|
+
try {
|
|
157
|
+
isTrusted = new URL(url).host === opts.trustedHost;
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!isTrusted && isBlockedUrl(url))
|
|
155
163
|
return null;
|
|
156
164
|
const res = await fetch(url, {
|
|
157
165
|
redirect: "manual",
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -153,6 +153,15 @@ export interface OgImageOptions {
|
|
|
153
153
|
sharp?: SharpOptions & JpegOptions;
|
|
154
154
|
takumi?: {
|
|
155
155
|
format?: 'png' | 'jpeg' | 'webp';
|
|
156
|
+
images?: Array<{
|
|
157
|
+
src: string;
|
|
158
|
+
data: ArrayBuffer | Uint8Array;
|
|
159
|
+
}>;
|
|
160
|
+
fetchedResources?: Array<{
|
|
161
|
+
src: string;
|
|
162
|
+
data: ArrayBuffer | Uint8Array;
|
|
163
|
+
}>;
|
|
164
|
+
/** @deprecated Use `images` instead. */
|
|
156
165
|
persistentImages?: Array<{
|
|
157
166
|
src: string;
|
|
158
167
|
data: ArrayBuffer;
|
|
@@ -229,6 +238,10 @@ export interface RuntimeCompatibilitySchema {
|
|
|
229
238
|
emoji?: 'local' | 'fetch';
|
|
230
239
|
wasm?: any;
|
|
231
240
|
}
|
|
241
|
+
export interface RuntimeCompatibilityMeta {
|
|
242
|
+
takumiVersion?: 1 | 2;
|
|
243
|
+
}
|
|
244
|
+
export type RuntimeCompatibilityPayload = Partial<Omit<RuntimeCompatibilitySchema, 'wasm'>> & RuntimeCompatibilityMeta;
|
|
232
245
|
export type CompatibilityFlags = Partial<Omit<RuntimeCompatibilitySchema, 'wasm'>>;
|
|
233
246
|
export interface CompatibilityFlagEnvOverrides {
|
|
234
247
|
dev?: CompatibilityFlags;
|