Skip to content

Commit 0a6f408

Browse files
authored
fix: resolve Cloudflare runtime og image secret (#596)
1 parent 2185ff0 commit 0a6f408

9 files changed

Lines changed: 184 additions & 6 deletions

File tree

src/runtime/app/utils.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { NuxtSSRContext } from 'nuxt/app'
33
import type { OgImageOptions, OgImageOptionsInternal, OgImagePrebuilt, OgImageRuntimeConfig } from '../types'
44
import { defu } from 'defu'
55
import { stringify } from 'devalue'
6-
import { useHead, useRuntimeConfig } from 'nuxt/app'
6+
import { useHead, useRequestEvent, useRuntimeConfig } from 'nuxt/app'
77
import { joinURL, withQuery } from 'ufo'
88
import { isRef, toValue } from 'vue'
99
import { componentNames } from '#build/nuxt-og-image/components.mjs'
@@ -310,9 +310,8 @@ export interface GetOgImagePathResult {
310310
* @deprecated Use the return value of `defineOgImage()` instead, which now returns an array of generated paths.
311311
*/
312312
export function getOgImagePath(_pagePath: string, _options?: Partial<OgImageOptionsInternal>): GetOgImagePathResult {
313-
const runtimeConfig = useRuntimeConfig()
314-
const baseURL = runtimeConfig.app.baseURL
315-
const { defaults, security } = useOgImageRuntimeConfig()
313+
const { app, defaults, security } = useOgImageRuntimeConfig()
314+
const baseURL = app.baseURL
316315
const extension = _options?.extension || defaults?.extension || 'png'
317316
// Force dynamic+signed URLs even during prerender when strict+secret are set.
318317
// Otherwise /_og/s/ URLs baked into HTML are unsigned and 403 at runtime for
@@ -337,13 +336,17 @@ export function getOgImagePath(_pagePath: string, _options?: Partial<OgImageOpti
337336
}
338337

339338
export function useOgImageRuntimeConfig() {
340-
const c = useRuntimeConfig()
339+
const event = import.meta.server ? useRequestEvent() : undefined
340+
const c = event ? useRuntimeConfig(event) : useRuntimeConfig()
341341
// Server-side: full runtime config at the root key.
342342
// Client-side: the root key is stripped (server-only); only the non-sensitive subset
343343
// published under `public['nuxt-og-image']` is available. The secret never crosses.
344344
const serverCfg = (c['nuxt-og-image'] as Record<string, any> | undefined) || {}
345345
const publicCfg = (c.public?.['nuxt-og-image'] as Record<string, any> | undefined) || {}
346346
const merged: Record<string, any> = { defaults: {}, ...publicCfg, ...serverCfg }
347+
const overrideSecret = (c as Record<string, any>).ogImage?.secret as string | undefined
348+
if (overrideSecret)
349+
merged.security = { ...(merged.security || {}), secret: overrideSecret }
347350
merged.app = { baseURL: c.app.baseURL }
348351
return merged as any as OgImageRuntimeConfig
349352
}

src/runtime/server/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@ export function getOgImagePath(_pagePath: string, _options?: Partial<OgImageOpti
3737
export function useOgImageRuntimeConfig(e?: H3Event) {
3838
const c = useRuntimeConfig(e)
3939
const moduleCfg = (c['nuxt-og-image'] as Record<string, any> | undefined) || {}
40+
const cloudflareEnv = e?.context.cloudflare?.env || (e?.context as any)?._platform?.cloudflare?.env
4041
// Top-level `ogImage.secret` is populated by Nuxt's standard env override
4142
// (`NUXT_OG_IMAGE_SECRET`) and takes precedence over the build-time
4243
// `security.secret` so deployments can rotate the secret without rebuilding.
4344
// Passing the event matters on platforms like Cloudflare Workers where env
4445
// bindings are only resolved when an event is available.
45-
const overrideSecret = (c as Record<string, any>).ogImage?.secret as string | undefined
46+
const overrideSecret = (
47+
(c as Record<string, any>).ogImage?.secret
48+
|| cloudflareEnv?.NUXT_OG_IMAGE_SECRET
49+
) as string | undefined
4650
const security = overrideSecret
4751
? { ...(moduleCfg.security || {}), secret: overrideSecret }
4852
: moduleCfg.security
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { pathToFileURL } from 'node:url'
2+
import { createResolver } from '@nuxt/kit'
3+
import { exec } from 'tinyexec'
4+
import { beforeAll, describe, expect, it } from 'vitest'
5+
import { ensureLocalModuleStub, extractOgImageUrl } from '../utils'
6+
7+
const { resolve } = createResolver(import.meta.url)
8+
const fixtureDir = resolve('../fixtures/cloudflare-runtime-config')
9+
10+
async function buildFixture() {
11+
await ensureLocalModuleStub()
12+
await exec('nuxt', ['build'], {
13+
nodeOptions: {
14+
cwd: fixtureDir,
15+
env: { ...process.env, NUXT_OG_IMAGE_SKIP_ONBOARDING: '1' },
16+
},
17+
})
18+
}
19+
20+
async function fetchWorker(path: string, env: Record<string, unknown>) {
21+
const workerPath = pathToFileURL(resolve(fixtureDir, '.output/server/index.mjs')).href
22+
const worker = await import(`${workerPath}?t=${Date.now()}`)
23+
return await worker.default.fetch(
24+
new Request(`https://example.com${path}`),
25+
env,
26+
{
27+
waitUntil() {},
28+
passThroughOnException() {},
29+
},
30+
)
31+
}
32+
33+
describe('cloudflare runtime config', () => {
34+
beforeAll(async () => {
35+
await buildFixture()
36+
}, 120000)
37+
38+
it('maps NUXT_OG_IMAGE_SECRET env bindings into event runtime config', async () => {
39+
const response = await fetchWorker('/api/runtime-config', {
40+
NUXT_OG_IMAGE_SECRET: 'cf-secret',
41+
})
42+
const body = await response.json()
43+
44+
expect(body.eventRuntimeConfig.ogImage.secret).toBe('cf-secret')
45+
expect(body.sharedRuntimeConfig.ogImage.secret).toBe('')
46+
expect(body.cloudflareEnv.NUXT_OG_IMAGE_SECRET).toBe('cf-secret')
47+
})
48+
49+
it('uses the Cloudflare runtime secret when rendering SSR og:image URLs', async () => {
50+
const response = await fetchWorker('/', {
51+
NUXT_OG_IMAGE_SECRET: 'cf-secret',
52+
})
53+
const html = await response.text()
54+
const ogImageUrl = extractOgImageUrl(html)
55+
56+
expect(ogImageUrl).toContain(',s_')
57+
})
58+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<NuxtPage />
3+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import NuxtOgImage from '../../../src/module'
2+
3+
export default defineNuxtConfig({
4+
modules: [
5+
NuxtOgImage,
6+
],
7+
8+
ogImage: {
9+
defaults: {
10+
renderer: 'satori',
11+
},
12+
},
13+
14+
nitro: {
15+
preset: 'cloudflare-module',
16+
},
17+
18+
devtools: { enabled: false },
19+
compatibilityDate: '2024-09-19',
20+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script setup lang="ts">
2+
import { defineOgImage } from '#imports'
3+
4+
defineOgImage('NuxtSeo.satori', {
5+
title: 'Cloudflare runtime config',
6+
})
7+
</script>
8+
9+
<template>
10+
<div>cloudflare runtime config</div>
11+
</template>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { defineEventHandler } from 'h3'
2+
import { useRuntimeConfig } from 'nitropack/runtime'
3+
4+
export default defineEventHandler((event) => {
5+
const runtimeConfig = useRuntimeConfig(event)
6+
const sharedRuntimeConfig = useRuntimeConfig()
7+
const cloudflareEnv = event.context.cloudflare?.env || event.context._platform?.cloudflare?.env
8+
9+
return {
10+
eventRuntimeConfig: summarize(runtimeConfig),
11+
sharedRuntimeConfig: summarize(sharedRuntimeConfig),
12+
cloudflareEnv: {
13+
keys: Object.keys(cloudflareEnv || {}).sort(),
14+
NUXT_OG_IMAGE_SECRET: cloudflareEnv?.NUXT_OG_IMAGE_SECRET,
15+
},
16+
}
17+
})
18+
19+
function summarize(runtimeConfig: Record<string, any>) {
20+
return {
21+
ogImage: runtimeConfig.ogImage,
22+
nuxtOgImageSecurity: runtimeConfig['nuxt-og-image']?.security,
23+
topLevelKeys: Object.keys(runtimeConfig).sort(),
24+
}
25+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "./.nuxt/tsconfig.json"
3+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
const runtimeConfig = {
4+
'app': {
5+
baseURL: '/',
6+
},
7+
'nuxt-og-image': {
8+
defaults: {},
9+
security: {
10+
strict: true,
11+
secret: '',
12+
},
13+
},
14+
'ogImage': {
15+
secret: '',
16+
},
17+
}
18+
19+
vi.mock('nitropack/runtime', () => ({
20+
useRuntimeConfig: () => runtimeConfig,
21+
}))
22+
23+
vi.mock('#og-image-virtual/component-names.mjs', () => ({
24+
componentNames: [],
25+
}))
26+
27+
describe('cloudflare runtime secret', () => {
28+
it('does not require an event outside Cloudflare request handling', async () => {
29+
const { useOgImageRuntimeConfig } = await import('../../src/runtime/server/utils')
30+
31+
const config = useOgImageRuntimeConfig()
32+
33+
expect(config.security.secret).toBe('')
34+
})
35+
36+
it('reads NUXT_OG_IMAGE_SECRET from Cloudflare env bindings', async () => {
37+
const { useOgImageRuntimeConfig } = await import('../../src/runtime/server/utils')
38+
39+
const config = useOgImageRuntimeConfig({
40+
context: {
41+
cloudflare: {
42+
env: {
43+
NUXT_OG_IMAGE_SECRET: 'cf-secret',
44+
},
45+
},
46+
},
47+
} as any)
48+
49+
expect(config.security.secret).toBe('cf-secret')
50+
})
51+
})

0 commit comments

Comments
 (0)