-
Notifications
You must be signed in to change notification settings - Fork 83
feat: add geocode proxy with CSRF protection for Google Maps billing optimization #635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b20ad54
7bfe9be
b9ef686
ce9fd98
b0201ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { useScriptGoogleMaps } from '#nuxt-scripts/registry/google-maps' | |||||||||||||||||||||||||||||||||||||||||||||||
| import { scriptRuntimeConfig } from '#nuxt-scripts/utils' | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { defu } from 'defu' | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { tryUseNuxtApp, useHead, useRuntimeConfig } from 'nuxt/app' | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { $fetch } from 'ofetch' | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { hash } from 'ohash' | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { withQuery } from 'ufo' | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { computed, onBeforeUnmount, onMounted, provide, ref, shallowRef, toRaw, watch } from 'vue' | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -132,6 +133,14 @@ const emits = defineEmits<{ | |||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = props.apiKey || scriptRuntimeConfig('googleMaps')?.apiKey | ||||||||||||||||||||||||||||||||||||||||||||||||
| const runtimeConfig = useRuntimeConfig() | ||||||||||||||||||||||||||||||||||||||||||||||||
| const proxyConfig = (runtimeConfig.public['nuxt-scripts'] as any)?.googleStaticMapsProxy | ||||||||||||||||||||||||||||||||||||||||||||||||
| const geocodeProxyConfig = (runtimeConfig.public['nuxt-scripts'] as any)?.googleGeocodeProxy | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Read CSRF token from cookie on client for geocode proxy requests | ||||||||||||||||||||||||||||||||||||||||||||||||
| function getCsrfToken(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (import.meta.server) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return '' | ||||||||||||||||||||||||||||||||||||||||||||||||
| return document.cookie.split('; ').find(c => c.startsWith('__nuxt_scripts_proxy='))?.split('=')[1] || '' | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Color mode support - try to auto-detect from @nuxtjs/color-mode | ||||||||||||||||||||||||||||||||||||||||||||||||
| const nuxtApp = tryUseNuxtApp() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -249,7 +258,19 @@ async function resolveQueryToLatLang(query: string) { | |||||||||||||||||||||||||||||||||||||||||||||||
| if (queryToLatLngCache.has(query)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return Promise.resolve(queryToLatLngCache.get(query)) | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| // only if the query is a string we need to do a lookup | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Use server-side geocode proxy when enabled to save Places API costs | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (geocodeProxyConfig?.enabled) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await $fetch<{ lat: number, lng: number }>('/_scripts/google-maps-geocode-proxy', { | ||||||||||||||||||||||||||||||||||||||||||||||||
| query: { input: query }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { 'x-nuxt-scripts-token': getCsrfToken() }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }).catch(() => null) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (data) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const latLng = new mapsApi.value!.LatLng(data.lat, data.lng) | ||||||||||||||||||||||||||||||||||||||||||||||||
| queryToLatLngCache.set(query, latLng) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return latLng | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+261
to
+272
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle case where The proxy path assumes Consider either awaiting Option 1: Return plain coordinates (avoids Maps API dependency) if (geocodeProxyConfig?.enabled) {
const data = await $fetch<{ lat: number, lng: number }>('/_scripts/google-maps-geocode-proxy', {
query: { input: query },
}).catch(() => null)
if (data) {
- const latLng = new mapsApi.value!.LatLng(data.lat, data.lng)
- queryToLatLngCache.set(query, latLng)
- return latLng
+ // Return LatLngLiteral - compatible with most Maps API methods
+ const latLng = { lat: data.lat, lng: data.lng }
+ queryToLatLngCache.set(query, latLng as any)
+ return latLng as any
}
}Option 2: Ensure Maps API is loaded first if (geocodeProxyConfig?.enabled) {
const data = await $fetch<{ lat: number, lng: number }>('/_scripts/google-maps-geocode-proxy', {
query: { input: query },
}).catch(() => null)
if (data) {
+ if (!mapsApi.value) {
+ await load()
+ await new Promise<void>((resolve) => {
+ const stop = watch(mapsApi, () => { stop(); resolve() })
+ })
+ }
const latLng = new mapsApi.value!.LatLng(data.lat, data.lng)
queryToLatLngCache.set(query, latLng)
return latLng
}
}π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| // Fallback to client-side Places API | ||||||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line no-async-promise-executor | ||||||||||||||||||||||||||||||||||||||||||||||||
| return new Promise<google.maps.LatLng>(async (resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!mapsApi.value) { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,124 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRuntimeConfig } from '#imports' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createError, defineEventHandler, getHeader, getQuery, getRequestIP, setHeader } from 'h3' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { $fetch } from 'ofetch' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { withQuery } from 'ufo' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { validateProxyCsrf } from './utils/proxy-csrf' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_INPUT_LENGTH = 200 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const RATE_LIMIT_WINDOW_MS = 60_000 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const RATE_LIMIT_MAX = 30 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requestCounts = new Map<string, { count: number, resetAt: number }>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function checkRateLimit(ip: string): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = Date.now() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entry = requestCounts.get(ip) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!entry || now > entry.resetAt) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requestCounts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry.count++ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return entry.count <= RATE_LIMIT_MAX | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memory leak: rate limit entries are never evicted (same issue as static maps proxy). This is the same unbounded memory growth issue present in π‘οΈ Proposed fix with periodic cleanup const requestCounts = new Map<string, { count: number, resetAt: number }>()
+
+// Clean up stale entries periodically (every 5 minutes)
+setInterval(() => {
+ const now = Date.now()
+ for (const [ip, entry] of requestCounts) {
+ if (now > entry.resetAt) {
+ requestCounts.delete(ip)
+ }
+ }
+}, 5 * 60 * 1000).unref()
function checkRateLimit(ip: string): boolean {Consider extracting the rate limiting logic into a shared utility module (e.g., π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default defineEventHandler(async (event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const runtimeConfig = useRuntimeConfig() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const publicConfig = (runtimeConfig.public['nuxt-scripts'] as any)?.googleGeocodeProxy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const privateConfig = (runtimeConfig['nuxt-scripts'] as any)?.googleGeocodeProxy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!publicConfig?.enabled) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 404, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusMessage: 'Google Geocode proxy is not enabled', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = privateConfig?.apiKey | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 500, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusMessage: 'Google Maps API key not configured for geocode proxy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // CSRF validation (double-submit cookie pattern) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validateProxyCsrf(event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Rate limit by IP | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ip = getRequestIP(event, { xForwardedFor: true }) || 'unknown' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!checkRateLimit(ip)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 429, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusMessage: 'Too many geocode requests', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Validate referer to prevent external abuse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const referer = getHeader(event, 'referer') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const host = getHeader(event, 'host') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (referer && host) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let refererHost: string | undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| refererHost = new URL(referer).host | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| catch {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (refererHost && refererHost !== host) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 403, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusMessage: 'Invalid referer', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Require origin metadata instead of skipping the check when it's absent. This only rejects requests when both π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const query = getQuery(event) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const input = query.input as string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!input) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 400, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusMessage: 'Missing "input" query parameter', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (input.length > MAX_INPUT_LENGTH) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 400, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusMessage: `Input too long (max ${MAX_INPUT_LENGTH} characters)`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+73
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate that
Suggested fix const query = getQuery(event)
- const input = query.input as string
+ const input = query.input
- if (!input) {
+ if (typeof input !== 'string' || !input.trim()) {
throw createError({
statusCode: 400,
statusMessage: 'Missing "input" query parameter',
})
}π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const googleUrl = withQuery('https://maps.googleapis.com/maps/api/place/findplacefromtext/json', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputtype: 'textquery', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fields: 'name,geometry', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key: apiKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await $fetch<{ candidates: Array<{ geometry: { location: { lat: number, lng: number } }, name: string }>, status: string }>(googleUrl, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'User-Agent': 'Nuxt Scripts Google Geocode Proxy', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }).catch((error: any) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: error.statusCode || 500, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusMessage: error.statusMessage || 'Failed to geocode query', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (data.status !== 'OK' || !data.candidates?.[0]?.geometry?.location) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusCode: 404, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusMessage: `No location found for "${input}"`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+108
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. User input is reflected in error messageβconsider sanitizing. The raw π‘οΈ Proposed fix to avoid reflecting user input if (data.status !== 'OK' || !data.candidates?.[0]?.geometry?.location) {
throw createError({
statusCode: 404,
- statusMessage: `No location found for "${input}"`,
+ statusMessage: 'No location found for the provided query',
})
}π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const location = data.candidates[0].geometry.location | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Cache aggressively - place names rarely change coordinates | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cacheMaxAge = publicConfig.cacheMaxAge || 86400 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setHeader(event, 'Content-Type', 'application/json') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setHeader(event, 'Cache-Control', `public, max-age=${cacheMaxAge}, s-maxage=${cacheMaxAge}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+117
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honor Using Suggested fix- const cacheMaxAge = publicConfig.cacheMaxAge || 86400
+ const cacheMaxAge = publicConfig.cacheMaxAge ?? 86400π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setHeader(event, 'Vary', 'Accept-Encoding') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { lat: location.lat, lng: location.lng, name: data.candidates[0].name } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Populate proxy API keys from the registry config before enabling the handlers.
Line 357 reads the key from
nuxt.options.runtimeConfig.public.scripts, but Lines 388-395 only mergeconfig.registryinto that object later. With the documented config shapescripts.registry.googleMaps = { apiKey: '...' }, both proxies are auto-enabled here and their private runtime config ends up withapiKey: undefined, so the handlers 500 at runtime. Read the key fromconfig.registry.googleMapsfirst, or compute this after the merge.π€ Prompt for AI Agents