6.2 KiB
Bridge, Security, and Native Capabilities
Use this when adding JavaScript-to-Zig calls, builtin commands, permissions, windows, child WebViews, dialogs, navigation policies, or external links.
Bridge architecture
JavaScript calls native Zig through:
const result = await window.zero.invoke("native.ping", { source: "webview" });
The runtime:
- Parses the JSON request.
- Enforces message size limits.
- Checks origin and permissions.
- Looks up a registered handler.
- Runs the handler and returns a JSON response.
Bridge commands are default-deny. A command must be registered in Zig and allowed by policy.
Handler pattern
fn ping(context: *anyopaque, invocation: native_sdk.bridge.Invocation, output: []u8) anyerror![]const u8 {
_ = invocation;
const self: *App = @ptrCast(@alignCast(context));
self.ping_count += 1;
return std.fmt.bufPrint(output, "{{\"message\":\"pong\",\"count\":{d}}}", .{self.ping_count});
}
Dispatcher pattern:
fn bridge(self: *App) native_sdk.BridgeDispatcher {
self.handlers = .{.{ .name = "native.ping", .context = self, .invoke_fn = ping }};
return .{
.policy = .{ .enabled = true, .commands = &policies },
.registry = .{ .handlers = &self.handlers },
};
}
When returning user-controlled strings, escape them:
return native_sdk.bridge.writeJsonStringValue(output, user_name);
Size limits
- Request message: 16 KiB.
- Response: 16 KiB.
- Handler result: 12 KiB.
- Request ID: 64 bytes.
- Command name: 128 bytes.
For large data, do not force everything through one bridge response. Use native files/resources or chunking patterns.
Security policy
Core defaults:
- No permissions granted unless listed.
- Bridge commands denied unless policy allows them.
- Navigation blocked unless origin is allowlisted.
- External links denied unless configured.
- Dialog builtin commands always denied unless explicitly listed in
builtin_bridge.
Manifest examples:
.permissions = .{ "window" },
.capabilities = .{ "webview", "js_bridge" },
.bridge = .{
.commands = .{
.{ .name = "native.ping", .origins = .{ "zero://app" } },
},
},
.security = .{
.navigation = .{
.allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" },
.external_links = .{ .action = "deny" },
},
},
Prefer exact origins over "*". Use "*" only for commands that expose no native state and only when the project already accepts that risk.
Builtin commands
The Native SDK includes builtin bridge commands for windows, layered WebViews, and dialogs. These are controlled separately from app-defined commands via builtin_bridge.
Window commands:
native-sdk.window.listnative-sdk.window.createnative-sdk.window.focusnative-sdk.window.close
Layered WebView commands:
native-sdk.webview.createnative-sdk.webview.listnative-sdk.webview.setFramenative-sdk.webview.navigatenative-sdk.webview.setZoomnative-sdk.webview.setLayernative-sdk.webview.close
Dialog commands:
native-sdk.dialog.openFilenative-sdk.dialog.saveFilenative-sdk.dialog.showMessage
Enable explicitly:
const app_permissions = [_][]const u8{native_sdk.security.permission_window};
.security = .{
.permissions = &app_permissions,
.navigation = .{ .allowed_origins = &.{ "zero://app" } },
},
.builtin_bridge = .{
.enabled = true,
.commands = &.{
.{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
.{ .name = "native-sdk.webview.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
.{ .name = "native-sdk.dialog.openFile", .origins = .{ "zero://app" } },
},
},
js_window_api = true exposes window.zero.windows.* and window.zero.webviews.*, but it does not bypass origin or permission checks.
Windows from JavaScript
const win = await window.zero.windows.create({
label: "tools",
title: "Tools",
width: 420,
height: 320,
});
const all = await window.zero.windows.list();
await window.zero.windows.focus(win.id);
await window.zero.windows.close(win.id);
Window state persistence uses stable labels. Use meaningful labels like main, settings, tools, or preview.
Layered WebViews
Child WebViews are native WebViews layered inside a native window:
const preview = await window.zero.webviews.create({
label: "preview",
url: "https://example.com",
frame: { x: 24, y: 24, width: 480, height: 320 },
layer: 10,
bridge: false,
});
await preview.setZoom(1.25);
await preview.setLayer(20);
await preview.close();
Rules:
- WebView URLs must pass navigation policy.
- Commands target only the calling native window.
mainis reserved for the startup WebView.- Child WebViews receive
window.zeroonly withbridge: true. - Backend gaps should reject with
invalid_request.
Dialogs
Dialogs require explicit builtin_bridge policy.
const files = await window.zero.invoke("native-sdk.dialog.openFile", {
title: "Select a file",
defaultPath: "/home",
allowMultiple: true,
allowDirectories: false,
});
const path = await window.zero.invoke("native-sdk.dialog.saveFile", {
title: "Save as",
defaultName: "untitled.txt",
});
const result = await window.zero.invoke("native-sdk.dialog.showMessage", {
style: "warning",
title: "Confirm",
message: "Delete this item?",
primaryButton: "Delete",
secondaryButton: "Cancel",
});
Use native dialogs for trusted app UI. Do not expose arbitrary filesystem access to remote or untrusted origins.
Error handling
JavaScript bridge calls reject with error.code:
invalid_request: malformed input, unsupported operation, denied navigation URL, missing target, duplicate/reserved label.unknown_command: no registered handler.permission_denied: origin or permission failed.handler_failed: handler returned an error.payload_too_large: request too large.internal_error: unexpected runtime failure.
Always handle errors in frontend code:
try {
await window.zero.invoke("native.save", payload);
} catch (error) {
console.error(error.code, error.message);
}