> ## Documentation Index
> Fetch the complete documentation index at: https://docs.overlayed.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Navigation Guard

> Every Overlayed window is protected against navigation takeovers — here's what's allowed, what's blocked, and how to customize it.

Every window created via `overlay.windows.createWindow` or `overlay.windows.createInGameWindow` is automatically
hardened against malicious navigation — most notably ad creatives that attempt to take over the window or open
unwanted sites. There is nothing to enable; this page describes the default policy and how to customize it.

## Default Policy

### Main-frame navigation

Top-frame navigation (`will-navigate` and main-frame redirects) is only permitted within **allowed origins**:

* Your application's own origin (the site URL configured in the [Overlayed Dashboard](https://overlay.dev))
* The origin currently loaded in the window — so local dev servers and custom URLs keep working
* Any hosts you list in [`allowedHosts`](#allowedhosts)

Anything else is blocked. A well-behaved SPA never navigates its top frame cross-origin, so a cross-origin top-frame
navigation is treated as a takeover attempt. A blocked navigation is **never** forwarded to the system browser.

<Info>
  Sub-frame navigations are intentionally untouched, so ad iframes keep loading cross-origin creatives normally.
</Info>

Redirects are only enforced for navigations initiated by the page itself. Calling `loadURL` with a URL that redirects
(for example, a loading page handing off to your site) is trusted and unaffected.

### New windows

`window.open` and `target="_blank"` links are handled per-URL:

| Target                              | Result                                                                                       |
| ----------------------------------- | -------------------------------------------------------------------------------------------- |
| An allowed origin                   | Opens in-app, like a regular Electron child window                                           |
| Any other `http`/`https` URL        | Opens in the **system browser** if the user just clicked or pressed a key, otherwise dropped |
| Any other scheme (`javascript:`, …) | Denied entirely                                                                              |

Opening unknown web URLs in the system browser preserves legitimate ad clickthroughs (advertiser domains are
unpredictable) while making an in-app takeover impossible. Windows opened in-app are guarded by the same policy (plus
the origin they were opened with), so the page that opened one can't redirect or navigate it somewhere else.

### User activation

Electron has no popup blocker. Unlike a browser, it lets any frame call `window.open` at any time, whether or not the
user clicked anything, which is exactly what malvertising does to force a redirect. The guard makes up for it: an
unknown web URL only opens in the system browser if the window received a click or key press in the last 5 seconds
(Chromium's own user activation lifespan), and each click opens at most one popup.

Once something in the window tries to open a popup without user activation, clicks made during the next 10 seconds don't
open popups, even after those 10 seconds are up. That way the offending frame can't ride the user's next click
somewhere else in the window.

<Info>
  Clicks inside cross-origin iframes (where most ad creatives render) are only visible to the guard on Electron 37 and
  later. On older versions, clicking such an ad does not open its landing page.
</Info>

### Tracking

Every blocked navigation and external open is logged with its URL and tracked via Cortex (`navigation_blocked`,
`window_open_external`, `window_open_denied`), so a malicious creative can be identified and reported to the ad
provider. `window_open_denied` includes a `reason`: `no_user_activation` and `popup_cooldown` point to a creative
opening popups on its own.

## Customizing the Policy

Configure the guard via the `navigationGuard` option of the `overlayed` function.

### allowedHosts

Hosts the main frame may navigate to in addition to the defaults. Use this for legitimate top-frame cross-origin flows
such as OAuth redirects. Matched against `URL.host` (hostname + port).

```typescript theme={null}
import { overlayed } from "@overlayed/app";

const overlay = overlayed({
	navigationGuard: {
		allowedHosts: ["auth.ubisoft.com"],
	},
	// ...
});
```

### windowOpenHandler

Overrides the default `window.open` policy per-URL. Return `"allow"` (open in-app), `"external"` (open in the system
browser, without requiring user activation), `"deny"` (block entirely), or `undefined` to fall back to the default
policy.

```typescript theme={null}
import { overlayed } from "@overlayed/app";

const overlay = overlayed({
	navigationGuard: {
		windowOpenHandler: ({ url }) => {
			if (new URL(url).host === "checkout.stripe.com") {
				return "allow";
			}
			return undefined; // fall back to the default policy
		},
	},
	// ...
});
```

<Warning>
  Prefer `windowOpenHandler` over calling `webContents.setWindowOpenHandler` yourself. Electron only supports one handler
  per window, so the guard wraps yours instead of letting it replace the guard. Your handler only runs for windows the
  guard would open: your `deny` stands and your window options apply to allowed origins, but an `allow` for any other
  origin still goes through the guard's policy. Set it after `overlay.windows.createWindow` returns: a handler set
  earlier (for example from `app.on("web-contents-created")`) is replaced when the window is registered.
</Warning>

<Info>
  As a safety net, `"external"` is only honored for `http`/`https` URLs — the guard never hands other schemes to the
  operating system, since that would launch whatever protocol handler is registered for them.
</Info>
