Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 3 additions & 110 deletions docs/content/docs/1.guides/2.first-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,118 +202,11 @@

## Proxy Endpoint Security

Several proxy endpoints (Google Static Maps, Geocode, Gravatar, embed image proxies) inject server-side API keys or forward requests to third-party services. Without protection, anyone who discovers these endpoints could call them directly and consume your API quota.
Proxy and embed endpoints (Google Static Maps, Geocode, Gravatar, embed image proxies) forward requests to third-party services. Each endpoint is restricted to an allowlist of upstream domains, so it cannot be used as an open proxy for arbitrary URLs.

Check warning on line 205 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "be used". Consider rewriting in active voice

Check warning on line 205 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "be used". Consider rewriting in active voice

### HMAC URL Signing
The Google Maps endpoints inject your server-side API key. They are cache-shielded (static maps for 7 days, geocode for 30 days), so repeated requests for the same map do not bill again. The endpoints are still reachable by anyone who discovers them; if quota abuse is a concern, add rate limiting at your platform edge or via Nitro `routeRules` for the `/_scripts/proxy/**` paths.

The module provides optional HMAC signing to lock down proxy endpoints. When enabled, only URLs generated server-side (during SSR or prerender) or accompanied by a valid page token are accepted. Unsigned requests receive a `403`.

#### Setup

Generate a signing secret:

```bash
npx @nuxt/scripts generate-secret
```

Then set it as an environment variable:

```bash
NUXT_SCRIPTS_PROXY_SECRET=<your-secret>
```

Or configure it directly:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
security: {
secret: process.env.NUXT_SCRIPTS_PROXY_SECRET,
}
}
})
```

#### How It Works

The module uses two verification modes:

1. **URL signatures** for server-rendered content. During SSR/prerender, proxy URLs include a `sig` parameter: an HMAC of the path and query params. The proxy endpoint verifies the signature before forwarding.

2. **Page tokens** for client-side reactive updates. Some components recompute their proxy URL after mount (e.g. measuring element dimensions). The server embeds a short-lived token (`_pt` + `_ts` params) in the SSR payload. The token is valid for any params on any proxy path and expires after 1 hour.

#### Development

In development, the module auto-generates a secret and writes it to your `.env` file on first run. You don't need to configure anything for local dev.

#### Production

Set `NUXT_SCRIPTS_PROXY_SECRET` in your deployment environment. The secret must be the same across all replicas and across build/runtime so that URLs signed at prerender time remain valid.

::callout{type="warning"}
Without a secret, proxy endpoints remain functional but unprotected. The module logs a warning at startup when it detects signed endpoints without a secret.
::

#### Signed Endpoints

The following proxy endpoints require signing when you configure a secret:

| Script | Endpoints |
|--------|-----------|
| **Google Maps** | `/_scripts/proxy/google-static-maps`, `/_scripts/proxy/google-maps-geocode` |
| **Gravatar** | `/_scripts/proxy/gravatar` |
| **Bluesky** | `/_scripts/embed/bluesky`, `/_scripts/embed/bluesky-image` |
| **Instagram** | `/_scripts/embed/instagram`, `/_scripts/embed/instagram-image`, `/_scripts/embed/instagram-asset` |
| **X (Twitter)** | `/_scripts/embed/x`, `/_scripts/embed/x-image` |

Analytics proxy endpoints (Google Analytics, Plausible, etc.) do not use signing because they only forward collection payloads and never expose API keys.

#### Configuration Reference

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
security: {
// HMAC secret for signing proxy URLs.
// Falls back to process.env.NUXT_SCRIPTS_PROXY_SECRET.
secret: undefined,
// Auto-generate and persist a secret to .env in dev mode.
// Set to false to disable.
autoGenerateSecret: true,
}
}
})
```

#### Troubleshooting

**Signed URLs return 403 after deploy**

The secret must be identical at build time (when URLs are signed during prerender) and at runtime (when the server verifies them). If you prerender pages, ensure `NUXT_SCRIPTS_PROXY_SECRET` is available in both your build environment and your deployment environment.

**403 errors across multiple replicas**

All server instances must share the same secret. If each replica generates its own secret, a URL signed by one instance will fail verification on another. Set `NUXT_SCRIPTS_PROXY_SECRET` as a shared environment variable across all replicas.

**Unexpected `NUXT_SCRIPTS_PROXY_SECRET` in `.env`**

The module only writes this when running `nuxt dev` with a signed endpoint enabled and no secret configured. If you only use client-side scripts (analytics, tracking), the module does not generate a secret. To prevent auto-generation entirely, set `autoGenerateSecret: false`.

**Page tokens expire**

Page tokens are valid for 1 hour. If a user leaves a tab open longer than that, client-side proxy requests will start returning 403. The page will recover on next navigation or refresh.

#### Static Generation and SPA Mode

URL signing requires a server runtime to verify HMAC signatures. Two deployment modes cannot support signing:

**`nuxt generate` (SSG) with static hosting**: Prerendered pages contain proxy URLs, but no Nitro server exists at runtime to verify signatures or forward requests. Proxy endpoints will not work on static hosts (GitHub Pages, Cloudflare Pages static, etc.). If you need proxy endpoints with prerendering, deploy to a server target that supports both prerendering and runtime request handling (e.g. Node, Cloudflare Workers, [Vercel](https://vercel.com)).

**`ssr: false` (SPA mode)**: No server-side rendering means no opportunity to sign URLs or embed page tokens. The signing secret lives in server-only runtime config and cannot be accessed from the client. Proxy endpoints still function if deployed with a server, but requests will be unsigned.

::callout{type="info"}
In both cases, the module automatically detects the limitation and skips signing setup. Proxy endpoints remain functional but unprotected. The module logs a warning at build time.
::
Analytics proxy endpoints (Google Analytics, Plausible, etc.) only forward collection payloads and never expose API keys.

## Supported Scripts

Expand Down
4 changes: 0 additions & 4 deletions docs/content/scripts/bluesky-embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ Nuxt Scripts provides a [`<ScriptBlueskyEmbed>`{lang="html"}](/scripts/bluesky-e
::script-docs{embed}
::

::callout{type="info"}
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
::

This registers the required server API routes (`/_scripts/embed/bluesky` and `/_scripts/embed/bluesky-image`) that handle fetching post data and proxying images.

## [`<ScriptBlueskyEmbed>`{lang="html"}](/scripts/bluesky-embed){lang="html"}
Expand Down
4 changes: 0 additions & 4 deletions docs/content/scripts/google-maps/2.api/1b.static-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ title: <ScriptGoogleMapsStaticMap>

Renders a [Google Maps Static API](https://developers.google.com/maps/documentation/maps-static) image. Use standalone for static map previews, or drop into the `#placeholder` slot of [`<ScriptGoogleMaps>`{lang="html"}](/scripts/google-maps/api/script-google-maps) for a loading placeholder.

::callout{type="info"}
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
::

::script-types{script-key="google-maps" filter="ScriptGoogleMapsStaticMap"}
::

Expand Down
4 changes: 0 additions & 4 deletions docs/content/scripts/google-maps/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,6 @@ You must add this. It registers server proxy routes that keep your API key serve
You can pass `api-key` directly on the `<ScriptGoogleMaps>`{lang="html"} component, but this approach is not recommended, as it exposes your key in client-side requests.
::

::callout{type="info"}
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
::

See [Billing & Permissions](/scripts/google-maps/guides/billing) for API costs and required permissions.

## Quick Start
Expand Down
4 changes: 0 additions & 4 deletions docs/content/scripts/gravatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ links:
::script-docs
::

::callout{type="info"}
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
::

## [`<ScriptGravatar>`{lang="html"}](/scripts/gravatar){lang="html"}

The [`<ScriptGravatar>`{lang="html"}](/scripts/gravatar){lang="html"} component renders a Gravatar avatar for a given email address. All requests are proxied through your server - Gravatar never sees your user's IP address or headers.
Expand Down
4 changes: 0 additions & 4 deletions docs/content/scripts/instagram-embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ Nuxt Scripts provides a [`<ScriptInstagramEmbed>`{lang="html"}](/scripts/instagr
::script-docs{embed}
::

::callout{type="info"}
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
::

This registers the required server API routes (`/_scripts/embed/instagram`, `/_scripts/embed/instagram-image`, and `/_scripts/embed/instagram-asset`) that handle fetching embed HTML and proxying images/assets.

## [`<ScriptInstagramEmbed>`{lang="html"}](/scripts/instagram-embed){lang="html"}
Expand Down
4 changes: 0 additions & 4 deletions docs/content/scripts/x-embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ Nuxt Scripts provides a [`<ScriptXEmbed>`{lang="html"}](/scripts/x-embed){lang="
::script-docs{embed}
::

::callout{type="info"}
This script's proxy endpoints use [HMAC URL signing](/docs/guides/first-party#proxy-endpoint-security) when you configure a `NUXT_SCRIPTS_PROXY_SECRET`. See the [security guide](/docs/guides/first-party#proxy-endpoint-security) for setup instructions.
::

This registers the required server API routes (`/_scripts/embed/x` and `/_scripts/embed/x-image`) that handle fetching tweet data and proxying images.

## [`<ScriptXEmbed>`{lang="html"}](/scripts/x-embed){lang="html"}
Expand Down
34 changes: 1 addition & 33 deletions packages/script/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
/**
* @nuxt/scripts CLI.
*
* Currently hosts a single command, `generate-secret`, which produces a
* cryptographically random HMAC secret for `NUXT_SCRIPTS_PROXY_SECRET`. This
* is an alternative to letting the module auto-write a secret into `.env`,
* for users who want explicit control (e.g. teams that commit secrets to a
* vault rather than `.env`).
*
* Keep this file zero-dependency: it runs standalone via `npx @nuxt/scripts`
* and should boot instantly.
*/

import { randomBytes } from 'node:crypto'
import process from 'node:process'

function generateSecret(): void {
const secret = randomBytes(32).toString('hex')
process.stdout.write(
[
'',
' @nuxt/scripts: proxy signing secret',
'',
` Secret: ${secret}`,
'',
' Add this to your environment:',
` NUXT_SCRIPTS_PROXY_SECRET=${secret}`,
'',
' The secret is automatically picked up by the module via runtime config.',
' It must be the same across all deployments and prerender builds so that',
' signed URLs remain valid.',
'',
'',
].join('\n'),
)
}

function showHelp(): void {
process.stdout.write(
[
Expand All @@ -44,8 +16,7 @@ function showHelp(): void {
' Usage: npx @nuxt/scripts <command>',
'',
' Commands:',
' generate-secret Generate a signing secret for proxy URL tamper protection',
' help Show this help',
' help Show this help',
'',
'',
].join('\n'),
Expand All @@ -57,9 +28,6 @@ const command = process.argv[2]
if (!command || command === 'help' || command === '--help' || command === '-h') {
showHelp()
}
else if (command === 'generate-secret') {
generateSecret()
}
else {
process.stderr.write(`Unknown command: ${command}\n`)
showHelp()
Expand Down
Loading
Loading