# Security

zero-native treats the WebView as untrusted by default. App authors opt into native power with explicit permissions, command policies, and navigation rules.

## Permissions and capabilities

`capabilities` describe broad features an app uses. `permissions` are the runtime grants checked before native commands run.

```zig
.permissions = .{ "window", "filesystem" },
.capabilities = .{ "webview", "js_bridge" },
```

### Available permissions

<table>
  <thead>
    <tr>
      <th>Permission</th>
      <th>Grants</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>window</code></td>
      <td>Window create/focus/close operations and layered WebView management</td>
    </tr>
    <tr>
      <td><code>filesystem</code></td>
      <td>File system access from bridge commands</td>
    </tr>
    <tr>
      <td><code>clipboard</code></td>
      <td>Clipboard read/write</td>
    </tr>
    <tr>
      <td><code>network</code></td>
      <td>Network requests from native code</td>
    </tr>
    <tr>
      <td><code>camera</code></td>
      <td>Camera access</td>
    </tr>
    <tr>
      <td><code>microphone</code></td>
      <td>Microphone access</td>
    </tr>
    <tr>
      <td><code>location</code></td>
      <td>Location services</td>
    </tr>
    <tr>
      <td><code>notifications</code></td>
      <td>System notifications</td>
    </tr>
  </tbody>
</table>

Custom permissions use reverse-DNS names (e.g. `com.example.my-permission`). Use the smallest set that covers your app.

### Available capabilities

<table>
  <thead>
    <tr>
      <th>Capability</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>webview</code></td>
      <td>WebView rendering</td>
    </tr>
    <tr>
      <td><code>js_bridge</code></td>
      <td>JavaScript bridge</td>
    </tr>
    <tr>
      <td><code>native_module</code></td>
      <td>Native Zig extension modules</td>
    </tr>
    <tr>
      <td><code>filesystem</code></td>
      <td>File system access</td>
    </tr>
    <tr>
      <td><code>network</code></td>
      <td>Network access</td>
    </tr>
    <tr>
      <td><code>clipboard</code></td>
      <td>Clipboard access</td>
    </tr>
  </tbody>
</table>

## Native commands

Native bridge commands are default-deny. A command must be registered by native code **and** allowed by policy before the runtime invokes it.

```zig
.bridge = .{
    .commands = .{
        .{
            .name = "native.ping",
            .origins = .{ "zero://app" },
        },
        .{
            .name = "zero-native.window.create",
            .permissions = .{ "window" },
            .origins = .{ "zero://app" },
        },
    },
},
```

Prefer exact origins over `"*"`. Use `"*"` only for local development or commands that do not expose native state.

## Builtin bridge policy

zero-native provides built-in commands for windows (`zero-native.window.*`), layered WebViews (`zero-native.webview.*`), and dialogs (`zero-native.dialog.*`). These are controlled separately from app-defined commands via the `builtin_bridge` field in `RuntimeOptions`.

`js_window_api` exposes the JavaScript window and WebView helpers, but it does not bypass security. Window commands (`zero-native.window.list`, `create`, `focus`, `close`) and WebView commands (`zero-native.webview.create`, `list`, `setFrame`, `navigate`, `setZoom`, `setLayer`, `close`) must come from an allowed origin and must have the `window` permission when runtime permissions are configured. WebView commands can only target the window that sent the bridge message. WebView URLs must also be allowed by `security.navigation.allowed_origins`, and child WebViews receive `window.zero` only when created with `bridge: true`.

For broader control, use an explicit `builtin_bridge` policy. When you choose this path, list every built-in command your app calls. Dialog commands (`zero-native.dialog.openFile`, `saveFile`, `showMessage`) are **always default-deny** and require an explicit `builtin_bridge` policy with the command listed:

```zig
.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.setFrame", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.navigate", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.setZoom", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.setLayer", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.close", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.dialog.openFile", .origins = .{ "zero://app" } },
        .{ .name = "zero-native.dialog.showMessage", .origins = .{ "zero://app" } },
    },
},
```

## Bridge error codes

When a bridge call fails, the JavaScript promise rejects with an error containing a `code` field:

<table>
  <thead>
    <tr>
      <th>Code</th>
      <th>Cause</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>invalid_request</code></td>
      <td>Malformed input, unsupported built-in operation, denied navigation URL, missing window/WebView, duplicate or reserved WebView label, or another caller-fixable request problem</td>
    </tr>
    <tr>
      <td><code>unknown_command</code></td>
      <td>No handler registered for this command</td>
    </tr>
    <tr>
      <td><code>permission_denied</code></td>
      <td>Origin or permission check failed</td>
    </tr>
    <tr>
      <td><code>handler_failed</code></td>
      <td>Handler returned an error</td>
    </tr>
    <tr>
      <td><code>payload_too_large</code></td>
      <td>Message exceeds 16 KiB limit</td>
    </tr>
    <tr>
      <td><code>internal_error</code></td>
      <td>Unexpected runtime error</td>
    </tr>
  </tbody>
</table>

Handle errors in JavaScript:

```javascript
try {
  const result = await window.zero.invoke("native.ping", {});
} catch (error) {
  console.error(error.code, error.message);
}
```

## Navigation policy

Main-frame navigation is allowlisted. Packaged assets normally use `zero://app`, inline examples use `zero://inline`, and dev servers should list their exact local origin.

```zig
.security = .{
    .navigation = .{
        .allowed_origins = .{
            "zero://app",
            "zero://inline",
            "http://127.0.0.1:5173",
        },
    },
},
```

Unknown main-frame navigations are blocked unless the external-link policy explicitly handles them.

## External links

External links are denied by default. To open links in the system browser, opt in and list URL prefixes:

```zig
.security = .{
    .navigation = .{
        .external_links = .{
            .action = "open_system_browser",
            .allowed_urls = .{ "https://example.com/docs/*" },
        },
    },
},
```

Do not allow broad external patterns for pages that can be influenced by remote content.

## CSP guidance

For packaged assets, start with a strict Content Security Policy:

```html
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'">
```

For inline Zig examples that embed scripts or styles, add only the minimum inline allowances:

```html
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'">
```

For dev servers, extend `connect-src` only to the local dev origin and WebSocket endpoint required by the framework. Keep production CSP separate from development CSP.

## Security model summary

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Default</th>
      <th>Opt-in</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>App bridge commands</td>
      <td>Denied</td>
      <td>Per-command policy with origin and permission checks</td>
    </tr>
    <tr>
      <td>Builtin bridge (windows and layered WebViews)</td>
      <td>Denied unless <code>js_window_api</code> or explicit policy allows the helper and origin/permission checks pass</td>
      <td><code>window</code> permission plus exact allowed origins</td>
    </tr>
    <tr>
      <td>Builtin bridge (dialogs)</td>
      <td>Denied</td>
      <td>Explicit <code>builtin_bridge</code> policy required</td>
    </tr>
    <tr>
      <td>Navigation</td>
      <td>Blocked</td>
      <td>Allowlisted origins</td>
    </tr>
    <tr>
      <td>External links</td>
      <td>Denied</td>
      <td>Explicit action + URL prefix list</td>
    </tr>
    <tr>
      <td>Permissions</td>
      <td>None granted</td>
      <td>Declared in <code>app.zon</code>, checked at runtime</td>
    </tr>
    <tr>
      <td>CSP</td>
      <td>Not enforced by zero-native</td>
      <td>Set in your HTML <code>&lt;meta&gt;</code> tag</td>
    </tr>
  </tbody>
</table>

The goal is defense in depth: even if a command is registered in Zig, it won't execute unless the policy allows it from the requesting origin with the required permissions.
