# Page Bridge & Injection

Source: https://inspectorlab.dev/page-bridge

> For the complete documentation index, see [llms.txt](https://inspectorlab.dev/llms.txt).

# Page Bridge & Injection

The page bridge is a fallback communication channel that allows Inspector Lab to function when the extension's background process cannot be reached. This is critical for environments like Orion on iOS and iPadOS where standard extension APIs may be unavailable or unreliable.

## Overview

Inspector Lab operates in multiple isolated worlds within a web page:

- **Isolated World**: Where the extension's content script runs (limited capabilities)
- **Main World**: The page's own JavaScript realm (full page access, but no extension APIs)
- **Inspector UI**: The devtools panel itself (in a shadow DOM)

The main world is necessary for certain operations—particularly console evaluation and network/console hooks—because only code running there can access the page's global scope and intercept its functions. Normally, the background service worker uses `chrome.scripting` to inject code into the main world. When that's unavailable, the page bridge takes over.

```mermaid
flowchart LR
    A["Inspector UI<br/>(Isolated World)"] -->|DOM Events| B["Page Bridge Client<br/>(Isolated World)"]
    B -->|CustomEvent| C["Page Bridge<br/>(Main World)"]
    C -->|Evaluation &<br/>Hook Connection| D["Page Runtime<br/>(Main World)"]
```

<Callout type="note">
The page bridge carries no extension privileges. Code executed through it has exactly the same authority as the page itself, and cannot bypass the page's Content Security Policy.
</Callout>

## Architecture

### Page Bridge Installation

The page bridge is loaded lazily when needed—typically when console evaluation or network capture is first requested and the background is unreachable. The `installPageBridge()` function in `page-bridge-client.ts` handles the installation:

<Steps>
  <Step title="Create Configuration">
    Generate four unique channel names (random tokens) for bidirectional communication: `connectEvent`, `evalRequestEvent`, `evalReplyEvent`, and `readyEvent`.
  </Step>
  <Step title="Inject Script">
    Create an inline `<script>` element containing the page bridge source code. The configuration is passed via the script's `data-inspector-lab-bridge` attribute as JSON.
  </Step>
  <Step title="Wait for Ready">
    Listen for the `readyEvent` custom event. The page bridge fires this as soon as it runs synchronously on insertion—or a security policy violation event fires if CSP blocks the injection.
  </Step>
  <Step title="Clean Up">
    Remove the script element from the DOM (it's no longer needed after execution) and resolve with a client interface.
  </Step>
</Steps>

**Timeout Behavior:**
- Install timeout: 5 seconds (`INSTALL_TIMEOUT_MS`)
- Eval timeout: 10 seconds per evaluation (`EVAL_TIMEOUT_MS`)

If the timeout expires or a `securitypolicyviolation` event fires for `script-src`, the install fails with a `PageBridgeError` describing the issue.

### Configuration Protocol

The bridge receives its configuration as a JSON object on the script element's dataset:

```ts
type PageBridgeConfig = {
  connectEvent: string;
  evalRequestEvent: string;
  evalReplyEvent: string;
  readyEvent: string;
};
```

This approach avoids any external dependencies or fetch operations—the bridge works even if the extension runtime has gone away entirely.

## Communication Flow

### Console Evaluation

When the inspector needs to evaluate an expression in the page's main world:

1. **Inspector UI** → calls `evaluateExpression()`
2. **Tries background first** → sends `EvaluateRequest` to background service worker
3. **On failure** → sets `evaluateViaPage = true` and installs the page bridge
4. **Page bridge client** → dispatches a `CustomEvent` on `evalRequestEvent`
5. **Page bridge (main world)** → receives the event and calls `evaluateInPage()`
6. **Evaluation result** → dispatched back as a custom event on `evalReplyEvent`
7. **Client resolves** → with `EvaluateResponse` (success or error message)

Each evaluation is assigned a unique ID to match requests with replies:

```ts
type PageBridgeEvalRequest = {
  id: string;
  expression: string;
  limit: number;  // Character limit for the preview
};

type PageBridgeEvalResponse = {
  id: string;
  response: EvaluateResponse;
};
```

### Three Core Services

The page bridge provides three services to replace what the background normally does in the main world:

1. **Console Hooks** — Installs and connects the console prehook (via `connect()`) so that `console.log()` and related calls are captured and sent to the inspector.

2. **Network Hooks** — Installs and connects the network prehook so that `fetch()` and `XMLHttpRequest` calls are captured without requiring the `webRequest` API.

3. **Expression Evaluation** — Evaluates arbitrary JavaScript in the page's global scope, running the same `evaluateInPage()` function that the background would inject.

All three services are automatically initialized when the prehooks are imported; both guard against duplicate installation, so this is a no-op when the document_start registration or an earlier launch already ran them.

### Hook Connection

Network and console capture relies on hooks installed in the main world (via `console-prehook` and `network-prehook` modules). When the background cannot inject these, the page bridge connects them instead:

```ts
type PageBridgeConnectRequest = {
  hook: "network" | "console";
  eventName: string;
};
```

The page bridge's `connect()` function:
1. Locates the hook on the `window` object (stored as `__inspectorLabNetworkHook` or `__inspectorLabConsoleHook`)
2. Sets its `eventName` property to the provided channel name
3. Replays any events buffered while the hook was unconnected

## Limitations and Fallbacks

### Content Security Policy

The most common failure point: if the page's CSP forbids inline scripts (`script-src` directive doesn't include `'unsafe-inline'`), the bridge cannot inject itself. The client detects this via a `securitypolicyviolation` event and immediately reports:

```text
This browser or this site's Content Security Policy blocks the in-page 
fallback, and the extension background is unreachable. Browser DevTools 
can bypass CSP; an in-page inspector cannot.
```

<Callout type="warning">
Browser DevTools can bypass a page's CSP because they run in a privileged context. Inspector Lab, running as a content script, cannot. This is a fundamental browser security boundary.
</Callout>

### Limited Cookie Access

When cookie operations fall back to `document.cookie`:

- **Available**: User-facing cookies (unencrypted, non-HttpOnly)
- **Unavailable**: HttpOnly cookies, Secure flag, SameSite settings

The inspector's **Cookies panel** labels fallback cookies as `"document"` source and explains the limitations.

### Network Capture

Network capture via the page bridge relies on `fetch` and `XMLHttpRequest` hooks. The `webRequest` API (which sees all network traffic) is not available, so:

- Real-time capture during page load is limited to JavaScript-initiated requests
- Service Worker requests and image loads may not be visible
- Headers-only details from the background (via `webRequest`) are not available as a fallback

## Cookie Fallback Logic

Cookie operations have a three-state flow:

```ts
let cookieStoreUnreachable: { reason: string; runtimeGone: boolean } | null = null;
```

1. **Initial state** (`null`): Try the background via `chrome.cookies`
2. **First read fails**: Note the reason, fall back to `document.cookie` for all subsequent reads and writes
3. **Write operations**: Only use the fallback if already marked unreachable; never trigger the fallback on their own

This prevents race conditions: a write timeout does not mean the message didn't arrive—it may have succeeded but lost its reply. Replaying the write against `document.cookie` could corrupt data.

**Timeout Optimization:**
Once a read has confirmed the store is unreachable, subsequent reads use a shorter timeout (1.5 seconds) instead of the default, so the panel doesn't stall on every refresh.

## XML and SVG Documents

The page bridge works in any document realm, including XML and SVG. The element-picking and inspection features work via DOM traversal (safe everywhere), but console evaluation and network hooks may behave differently in non-HTML contexts due to differences in the global object and available APIs.

See [XML & SVG Document Support](/xml-svg-support) for more details.

## Error Handling

### `PageBridgeError`

Thrown by `installPageBridge()` when injection fails. Always carries a user-friendly message suitable for display in the UI, since no JavaScript console is available to the end user on devices like iPad.

```ts
export class PageBridgeError extends Error {}
```

### Silent Failures

Some failures are intentionally silent to avoid noise:

- Invalid event details (not JSON) are ignored
- A mismatched `id` in an eval reply is ignored (may belong to a timed-out request)
- If the bridge is called on a page that never installed the prehooks, `connect()` is a no-op

## Performance Notes

- **Script injection overhead**: ~0–5ms (synchronous execution)
- **Per-evaluation overhead**: 1–2ms for event dispatch and JSON serialization
- **Memory**: ~12 KB of source code, removed after successful installation
- **No persistent state**: Each page load gets a fresh bridge with new channel tokens

The bridge is a fallback, not a replacement: use the background path whenever possible. Chrome and Firefox with a healthy extension runtime should never hit this code path.

## Related Pages

<Columns cols={2}>
  <Card title="Message Protocol" icon="message-square" href="/messaging">Learn how the inspector and background communicate.</Card>
  <Card title="Architecture Overview" icon="layers" href="/architecture">Understand the full system design.</Card>
  <Card title="Console Panel" icon="terminal" href="/panels-console">See how evaluation is used in practice.</Card>
  <Card title="Network Panel" icon="globe" href="/panels-network">Explore network capture architecture.</Card>
</Columns>
