Skip to content
Merged
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
115 changes: 115 additions & 0 deletions docs/content/docs/1.guides/2.first-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@

### Static Hosting (SSG)

The reverse proxy requires a **server runtime**. For static deployments (`nuxt generate`), the proxy is automatically disabled. Scripts are still bundled and served from your domain, but runtime collection requests (analytics beacons, pixel fires) go directly to third-party servers.

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

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

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

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

If you need proxying with static hosting, configure platform-level rewrites manually. The pattern is `/_scripts/p/<domain>/:path*` β†’ `https://<domain>/:path*`:

Expand All @@ -200,6 +200,121 @@
Platform-level rewrites bypass the privacy anonymisation layer. The proxy handler only runs in a Nitro server runtime.
::

## 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.

### HMAC URL Signing

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.
::

## Supported Scripts

### Full First-Party (Bundled + Proxied)
Expand Down Expand Up @@ -346,7 +461,7 @@
| Problem | Fix |
|---------|-----|
| Analytics not tracking | Check DevTools β†’ Network for `/_scripts/p/` requests. Check Nitro server logs for proxy errors |
| Proxy not working on static site | The reverse proxy is automatically disabled for SSG. Use platform rewrites or switch to server mode. See [Static Hosting](#static-hosting-ssg) |

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

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

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

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice
| Stale script | `rm -rf .nuxt/cache/scripts` and rebuild |
| Build download fails | Set `assets.fallbackOnSrcOnBundleFail: true`{lang="ts"} to fall back to direct loading |
| Debugging | Open Nuxt DevTools β†’ Scripts to see proxy routes and privacy status |
Expand Down
4 changes: 4 additions & 0 deletions docs/content/scripts/bluesky-embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ 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: 4 additions & 0 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,6 +4,10 @@ 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: 4 additions & 0 deletions docs/content/scripts/google-maps/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ 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: 4 additions & 0 deletions docs/content/scripts/gravatar.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ 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: 4 additions & 0 deletions docs/content/scripts/instagram-embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ 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: 4 additions & 0 deletions docs/content/scripts/x-embed.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ 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
2 changes: 2 additions & 0 deletions packages/script/bin/cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import('../dist/cli.mjs')
1 change: 1 addition & 0 deletions packages/script/build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default defineBuildConfig({
'./src/registry',
'./src/stats',
'./src/types-source',
'./src/cli',
],
externals: [
'nuxt',
Expand Down
4 changes: 4 additions & 0 deletions packages/script/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@
]
}
},
"bin": {
"nuxt-scripts": "./bin/cli.mjs"
},
"files": [
"bin",
"dist"
],
"scripts": {
Expand Down
67 changes: 67 additions & 0 deletions packages/script/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @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(
[
'',
' @nuxt/scripts CLI',
'',
' Usage: npx @nuxt/scripts <command>',
'',
' Commands:',
' generate-secret Generate a signing secret for proxy URL tamper protection',
' help Show this help',
'',
'',
].join('\n'),
)
}

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()
process.exit(1)
}
Loading
Loading