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
135 changes: 3 additions & 132 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,15 @@ 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.

### HMAC URL Signing

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.
Embed, avatar, and analytics proxy routes are public resources. They do not contain server-side API keys. Each route accepts only the upstream hosts and request shapes declared by its integration.

::callout{type="warning"}
Runtime proxy fetches validate the initial upstream URL and every redirect target before requesting it. Direct local, private, link-local, and reserved targets are rejected on every runtime; Node deployments also validate and pin DNS results before opening the socket. Image routes reject active content types such as HTML and SVG. The Instagram embed route restricts post and stylesheet hosts, then sanitizes the returned fragment before client rendering.
::

#### 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.
Nuxt Scripts does not proxy Google Maps requests. Static Maps loads from Google with the public browser key, while location lookup uses the Maps JavaScript Places service. Apply website and API restrictions to the key, then configure Google Cloud quotas to cap spend. See [Google Maps Platform security guidance](https://developers.google.com/maps/api-security-best-practices).

**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-authorization boundary 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.
::
If public embed traffic needs request limits, configure them at your deployment edge or add application middleware. Nitro 2 route rules do not provide a portable rate limiter.

## Supported Scripts

Expand Down
23 changes: 0 additions & 23 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 Expand Up @@ -250,10 +234,3 @@ Cache duration for bundled scripts in milliseconds. Scripts older than this will
Generates a Subresource Integrity (SRI) hash for each bundled script and adds `integrity` with `crossorigin="anonymous"`.

Browsers compare the downloaded script with its declared hash before executing it; see MDN's [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity) guide.

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

- Type: `{ enabled?: boolean, cacheMaxAge?: number }`{lang="ts"}
- Default: `{ enabled: false, cacheMaxAge: 3600 }`

Controls the legacy Google Static Maps proxy switch and its response cache duration in seconds. Registering `googleMaps` also enables the Static Maps and geocoding endpoints. See the [Google Maps Static Map API](/scripts/google-maps/api/static-map) for component usage.
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
20 changes: 5 additions & 15 deletions docs/content/scripts/google-maps/2.api/1b.static-map.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
---
title: <ScriptGoogleMapsStaticMap>
description: Render a Google Maps Static API image directly or through the built-in server proxy.
description: Render a Google Maps Static API image directly from Google.
---

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

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

Expand Down Expand Up @@ -75,7 +67,7 @@ Use inside [`<ScriptGoogleMaps>`{lang="html"}](/scripts/google-maps/api/script-g
| `language` | `string` | | Language code for map labels. |
| `region` | `string` | | Region bias. |
| `signature` | `string` | | [Google Maps URL signature](https://developers.google.com/maps/digital-signature) for a direct request. Use it with an explicit `apiKey`; a signature does not replace the key. |
| `apiKey` | `string` | | API key override. Takes priority over the proxy; the component falls back to the server-side key when omitted. |
| `apiKey` | `string` | | API key override. Falls back to the public `googleMaps` registry key when omitted. |
| `width` | `number \| string` | `640` | CSS width for the container. |
| `height` | `number \| string` | `400` | CSS height for the container. |
| `loading` | `'eager' \| 'lazy'` | `'lazy'` | Image loading strategy. |
Expand All @@ -92,14 +84,12 @@ When `size` is not provided, the component:

Set `size` explicitly to bypass auto-measurement.

## Proxy Support

Configuring `googleMaps` in `scripts.registry` enables a server-side proxy automatically. The component routes requests through it unless you provide an explicit `apiKey` prop. Passing the prop produces a direct `https://maps.googleapis.com/maps/api/staticmap` image URL.
## API Key Security

The built-in proxy cannot use a client-provided `signature` because the server appends the API key after it receives the query. Pass both `apiKey` and `signature` to make a signed direct request.
The component loads the image directly from `maps.googleapis.com`. Restrict the key to your website and the Maps Static API before production use. See [Google Maps Platform security guidance](https://developers.google.com/maps/api-security-best-practices).

::callout{color="amber"}
The component currently builds its proxy URL with the default `/_scripts` prefix. If you configure a custom `scripts.prefix`, static-map proxy requests still use `/_scripts/proxy/google-static-maps` and will miss the relocated route. Pass `apiKey` to use Google's direct URL, or keep the default prefix until this is fixed.
Google digital signatures cover the exact request URL. When passing `signature`, also pass an explicit `size` and keep every signed parameter stable. Generate the signature on a trusted server. Never expose the URL signing secret to the browser.
::

## Slots
Expand Down
17 changes: 2 additions & 15 deletions docs/content/scripts/google-maps/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Google Maps
description: Load interactive maps on demand and proxy Static Maps or geocoding requests.
description: Load interactive maps, static maps, and location search on demand.
links:
- label: useScriptGoogleMaps
icon: i-simple-icons-github
Expand Down Expand Up @@ -47,23 +47,10 @@ export default defineNuxtConfig({
NUXT_PUBLIC_SCRIPTS_GOOGLE_MAPS_API_KEY=<YOUR_API_KEY>
```

Registering Google Maps also adds server proxy routes that keep the key out of static-map and geocoding request URLs:

- `/_scripts/proxy/google-static-maps` for placeholder images
- `/_scripts/proxy/google-maps-geocode` for location search

Add `trigger: 'onNuxtReady'` to the registry entry only when you want the interactive Maps API to load globally. It bypasses the component's default interaction delay because the shared script instance is already loading.

::callout{color="amber"}
The Maps JavaScript API still sends the key to the browser when the interactive map loads. Follow Google's [API security guidance](https://developers.google.com/maps/api-security-best-practices): restrict keys by application and API, and use separate keys for client-side and server-side services when possible. Passing `api-key` directly on `<ScriptGoogleMaps>`{lang="html"} also exposes it in the client bundle, whereas runtime config lets you vary the key by deployment.
::

::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. 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.
The Maps JavaScript and Static Maps APIs send this key to the browser. Follow Google's [API security guidance](https://developers.google.com/maps/api-security-best-practices): apply a Websites application restriction, allow only the APIs this site uses, and configure quota limits. Runtime config keeps deployment values configurable; it does not make a `NUXT_PUBLIC_` key secret.
::

See [Billing & Permissions](/scripts/google-maps/guides/billing) for API costs and required permissions.
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
8 changes: 7 additions & 1 deletion packages/script/bin/cli.mjs
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
#!/usr/bin/env node
import('../dist/cli.mjs')
import process from 'node:process'
import { runCli } from '../dist/cli.mjs'

process.exitCode = runCli(process.argv.slice(2), {
writeStdout: value => process.stdout.write(value),
writeStderr: value => process.stderr.write(value),
})
Loading
Loading