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
139 changes: 3 additions & 136 deletions docs/content/docs/1.guides/2.first-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,144 +256,11 @@ Platform-level rewrites bypass the privacy anonymization layer. The proxy handle

## Proxy Endpoint Security

Some proxy endpoints inject server-side API keys or forward arbitrary resource requests. This includes Google Static Maps, Geocode, Gravatar, and embed image proxies. Anyone can call an unprotected endpoint 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 restricts upstream requests to an allowlist of domains, preventing arbitrary proxy use.

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

Optional HMAC signing accepts either an exact URL generated during SSR or prerender, or a request carrying a valid page token. Requests with neither credential receive a `403`. The [signing implementation](https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/server/utils/sign.ts) canonicalizes each URL before generating its HMAC-SHA256 signature.

#### Setup

Generate a signing secret with the CLI:

```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,
}
}
})
```

#### Verification Modes

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.

Page tokens are deliberately broader than URL signatures: anyone who can read a valid token can change the parameters and signed proxy path until it expires. Treat them as short-lived authorization for the proxy group, not proof that a request matches an exact server-generated URL.

#### Development

In development, the module generates a secret and writes it to `.env` on the first run.

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

The generic analytics proxy does not use signing. It accepts only upstream domains registered at build time and does not inject the protected API keys used by the signed endpoints above.

#### 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,
// Page-token lifetime in seconds (default: 3600).
pageTokenMaxAge: 3600,
}
}
})
```

To disable proxy security entirely, set `security` to `false`:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
scripts: {
// No secret is resolved or auto-generated, no page token is added to the
// SSR payload, and proxy endpoints pass requests through unverified.
security: false,
}
})
```

Disable security when you need a deterministic SSR payload, such as one used to compute a stable response `etag`. Without it, proxy endpoints still work but remain open to quota abuse and arbitrary requests to their allowlisted upstreams.

::callout{type="warning"}
The shared [image-proxy handler](https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/server/utils/image-proxy.ts) checks the initial URL's scheme and allowed hostname. Several embed image and asset routes then follow upstream redirects without checking each redirect target again. This is an implementation boundary, not evidence that a configured vendor host is exploitable: keep proxy security enabled and do not treat the initial-host allowlist as complete redirect-chain validation.
::

#### 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 by default. If a user leaves a tab open longer than `security.pageTokenMaxAge`, client-side proxy requests will start returning 403. The page will recover on the next navigation or refresh.

**Proxy token changes the response payload on every request**

The module injects a per-request page token into the SSR payload, so the response hash differs each request. If you compute a stable `etag`, set `security: false` to disable proxy security entirely. Proxy endpoints then pass requests through without signature verification, so only do this if you accept the wider request and redirect-validation boundaries described above.

#### 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 such as GitHub Pages. If you need proxy endpoints alongside prerendered pages, deploy to a server target that supports runtime request handling; [Vercel supports both static and server-rendered Nuxt deployments](https://vercel.com/docs/frameworks/full-stack/nuxt).

**`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"}
The module skips signing setup and logs a build warning in both cases. In SPA mode with a deployed server, registered endpoints remain available without signature checks. A fully static host has no runtime endpoint to receive the request.
::
Analytics proxy endpoints (Google Analytics, Plausible, etc.) only forward collection payloads and never expose API keys.

## Supported Scripts

Expand Down
16 changes: 0 additions & 16 deletions docs/content/docs/3.api/5.nuxt-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,6 @@ export default defineNuxtConfig({
})
```

## `security`{lang="ts"}

- Type: `false | { secret?: string, autoGenerateSecret?: boolean, pageTokenMaxAge?: number }`{lang="ts"}
- Default: `undefined` (the module configures signing after it registers a protected endpoint)

Configures HMAC protection for proxy endpoints that expose server-side API keys or forward arbitrary external resources. The secret falls back to `NUXT_SCRIPTS_PROXY_SECRET`. In development, the module generates and persists a secret when you enable a signed endpoint without providing one.

Production does not auto-generate a secret. If the module finds a protected endpoint without one, it warns and leaves that endpoint functional but unsigned. URL signing is also unavailable for `ssr: false` and static Nitro presets because they have no server runtime to verify signatures.

- `security.secret`{lang="ts"}: the HMAC secret
- `security.autoGenerateSecret`{lang="ts"}: whether development may create the secret; defaults to `true`
- `security.pageTokenMaxAge`{lang="ts"}: client page-token lifetime in seconds; defaults to `3600`
- `security: false`{lang="ts"}: disables signing and lets registered endpoints accept unsigned requests

See [Proxy Endpoint Security](/docs/guides/first-party#proxy-endpoint-security) for setup and deployment constraints.

## Partytown (Web Worker) :badge[Experimental]{color="amber"}

Load individual scripts in a web worker using [Partytown](https://partytown.qwik.dev/). A registry `trigger` is still required to generate a global call, but it does not defer the Partytown tag: the current implementation writes that tag into the server-rendered HTML.
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 @@ links:
::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.
::

Enabling the integration registers `/_scripts/embed/bluesky` for post data and `/_scripts/embed/bluesky-image` for 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 @@ -5,10 +5,6 @@ description: Render a Google Maps Static API image directly or through the built

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

::callout{color="amber"}
Google's [Maps Platform FAQ](https://developers.google.com/maps/faq#static_map) requires browser pages to load Static Maps images directly from Google rather than store and serve copies. Because the built-in proxy caches and serves the image, pass an explicit `api-key` to bypass it and review the current Google Maps terms for your use case.
::
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 @@ -62,10 +62,6 @@ The Maps JavaScript API still sends the key to the browser when the interactive
Google's [Maps Platform FAQ](https://developers.google.com/maps/faq#static_map) requires browser pages to load Static Maps images directly from Google. The current static-map proxy caches and serves those images, so pass an explicit `api-key` to `<ScriptGoogleMapsStaticMap>`{lang="html"} to bypass the proxy and review the Maps Platform terms before using that component.
::

::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. The avatar image request is proxied through your server, so Gravatar does not receive the user's IP address from that request.
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 @@ links:
::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 @@ links:
::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