- 桌面应用
- 跨平台
【免费下载链接】native
Toolkit for building native desktop apps
导读
本文是 Native SDK(一个用于构建原生桌面应用的跨平台工具包)中「桥接(Bridge)、安全与原生能力」的完整技术指南,讲解 JavaScript 前端如何通过window.zero.invoke()调用原生 Zig 代码,以及桥接命令、内置命令、权限、窗口、子 WebView、对话框、导航策略与外部链接如何在一个"默认拒绝(default-deny)"的安全模型下协同工作。读完本文,你将能:注册并暴露自己的原生桥接命令、按命令与来源(origin)精确配置安全策略、从 JavaScript 启用窗口/分层 WebView/原生对话框等内置能力,并掌握全部错误码与限制,写出安全且可维护的混合应用。
本文以 skill-data/core/references/bridge-security-native-capabilities.md 为核心骨架,并结合仓库源码(src/bridge/root.zig、src/security/root.zig、src/runtime/builtin_bridge.zig 等)与类型声明(packages/native-sdk/native-sdk.d.ts)进行源码级佐证。
Bridge 架构:JavaScript 如何调用原生 Zig
在 Native SDK 中,JavaScript 通过一个统一的异步调用入口访问原生能力:
const result = await window.zero.invoke("native.ping", { source: "webview" });window.zero.invoke(command, payload)是前端与原生世界的唯一信道:command是已注册的桥接命令名,payload是任意 JSON 值。该调用会打包为一个请求包进入运行时(Runtime),由运行时统一完成解析、检查、路由与响应。
运行时处理一次调用遵循固定的 5 步流程:
- 解析 JSON 请求:拆出
id、command与原始payload三个字段。 - 强制消息大小限制:超限请求直接以
payload_too_large拒绝。 - 检查来源(origin)与权限:命令策略要求来源匹配、权限齐备,否则拒绝。
- 查找已注册的处理器(handler):在注册表中按命令名查找;找不到返回
unknown_command。 - 执行处理器并返回 JSON 响应:处理器结果经 JSON 合法性校验后封包返回。
这五步在源码中一一对应 src/bridge/root.zig 的Dispatcher.dispatch():先做max_message_bytes检查,再parseRequest(),随后policy.allows()做策略校验,接着registry.find()查处理器,最后执行handler.invoke_fn并用writeSuccessResponse包装结果(src/bridge/root.zig#L142-L164)。
关键安全前提是桥接命令默认拒绝(default-deny):一个命令必须同时满足"已在 Zig 中注册"与"被策略允许"两个条件,二者缺一不可。即便代码里注册了处理器,策略未放行时调用方仍会收到permission_denied。
Handler 模式:在 Zig 中注册桥接命令
应用侧在 Zig 中编写处理器函数,签名固定为接收context、invocation与输出缓冲区:
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}); }要点:
context指向应用状态(本例中借它自增ping_count),通过@ptrCast(@alignCast(context))还原为*App;output是运行时提供的结果缓冲区,处理器必须把合法 JSON写进去并返回切片;invocation携带请求详情(request与source,其中source.origin是调用来源),不需要时用_ = invocation忽略。
接着用 Dispatcher 模式把处理器与策略组装起来:
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 }, }; }其中policies是需要单独定义的命令策略数组(见下文"安全策略")。Dispatcher在源码中是policy + registry + async_registry的组合(src/bridge/root.zig),还支持异步处理器AsyncHandler(通过AsyncResponder.success/fail延迟应答,适合耗时操作)。
返回用户可控字符串时务必转义,否则会产生"注入无效 JSON"乃至响应走私风险。正确写法是:
return native_sdk.bridge.writeJsonStringValue(output, user_name);writeJsonStringValue会把字符串写成合法 JSON 字符串字面量(含引号、反斜杠转义)。仓库测试src/bridge/root.zig#L408-L421验证了这一点:未经转义的hello "user"会被判定为非法 JSON 并以handler_failed拒绝(writeSuccessResponse内部先做json.isValidValue校验,非法即降级为错误响应)。
消息大小限制
桥接信道对每条消息都有硬性上限,超出即拒绝:
| 项 | 限制 |
|---|---|
| 请求消息(request message) | 16 KiB |
| 响应(response) | 16 KiB |
| 处理器结果(handler result) | 12 KiB |
| 请求 ID | 64 字节 |
| 命令名(command name) | 128 字节 |
需要说明的是,skill 文档给出的 16 KiB 是编写桥接代码时应遵循的安全默认值;运行时底层还定义了更宽的物理上限——src/bridge/root.zig 中max_message_bytes、max_response_bytes、max_result_bytes均为 1 MiB,max_id_bytes = 64、max_command_bytes = 128,命令名与 ID 上限与文档一致。parseRequest与validId/validCommand会分别校验 ID 与命令名的字符集(禁止控制字符、引号、反斜杠、空格与/,见 src/bridge/root.zig)。
大数据传输建议:不要把大数据硬塞进单条桥接响应。正确做法是:
- 将大数据写入原生文件/资源,通过桥接只传递引用(路径、ID);
- 或采用分块(chunking)模式,将数据切分为多条小消息分批传输;
- 避免把整个文件内容、大型图片或日志塞进一个
invoke返回值。
安全策略:默认拒绝,精确放行
核心默认值
Native SDK 的安全模型以"最小权限"为出发点,以下是默认行为,未配置即拒绝:
- 不授予任何权限,除非在权限列表中显式列出;
- 拒绝所有桥接命令,除非策略允许该命令;
- 阻止导航,除非目标 origin 在导航允许列表中;
- 拒绝外部链接,除非显式配置;
- 始终拒绝对话框内置命令,除非在
builtin_bridge中显式列出。
Manifest 配置示例
桥接命令与安全策略既可写在应用清单(app.json/app.zon),也可在运行时的策略对象中直接配置。以下为清单(ZON 语法)示例:
.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" }, }, },逐项解读:
permissions:应用级权限,例如"window"、"filesystem"、"clipboard"、"notifications"等(完整常量见 src/security/root.zig:permission_window、permission_command、permission_view、permission_dialog、permission_filesystem、permission_clipboard、permission_network、permission_notifications、permission_credentials);capabilities:声明应用启用的能力域,例如"webview"(WebView 层)、"js_bridge"(JS 桥接)、"native_views"、"gpu_surfaces"等(完整枚举见 packages/native-sdk/schemas/app.schema.json);bridge.commands:命令级策略,每条含name、可选的permissions与origins;security.navigation.allowed_origins:主框架导航允许的 origin,本地应用内容常用zero://app,开发服务器常用http://127.0.0.1:5173;security.navigation.external_links:外部链接策略,action可选"deny"或"open_system_browser"(src/security/root.zig 定义了ExternalLinkAction与ExternalLinkPolicy;allowsExternalUrl支持精确 URL 与带*前缀通配,且通配必须包含合法 scheme 与路径前缀,防止example.com.evil绕过,见 src/security/root.zig)。
Origin 与权限的判定逻辑
策略判定在源码中由Policy.allows()完成(src/bridge/root.zig):
enabled为 false 直接拒绝;- 未在
commands中找到命令名则拒绝; - 策略要求的每个权限都必须在运行时权限集中(
security.hasPermissions,要求全部满足,见 src/security/root.zig)——注意:若策略本身配置了permissions列表,会与运行时权限取交集校验; - origin 校验:
origins为空表示任意来源;否则逐条精确匹配,"*"放行一切。
实践原则:优先使用精确 origin,而非"*"。"*"只应留给"不暴露任何原生状态"的命令,且仅当项目已经明确接受该风险时使用。这与 skill-data/core/SKILL.md 中"偏好精确的安全策略变更而非宽泛放行"的总体准则一致。
内置命令:窗口、分层 WebView 与对话框
除应用自定义命令外,Native SDK 自带一批内置桥接命令,覆盖窗口、分层 WebView 与对话框。它们与应用自定义命令分开控制:必须通过builtin_bridge显式启用。
窗口命令(Window)
native-sdk.window.listnative-sdk.window.createnative-sdk.window.focusnative-sdk.window.close
分层 WebView 命令(Layered WebView)
native-sdk.webview.createnative-sdk.webview.listnative-sdk.webview.setFramenative-sdk.webview.navigatenative-sdk.webview.setZoomnative-sdk.webview.setLayernative-sdk.webview.close
对话框命令(Dialog)
native-sdk.dialog.openFilenative-sdk.dialog.saveFilenative-sdk.dialog.showMessage
这些命令的底层分发实现在 src/runtime/builtin_bridge.zig:dispatchWindowBridgeCommand(窗口四命令)、dispatchWebViewBridgeCommand(WebView 七命令,且原生-only 构建会以WebViewLayerNotBuilt教学式错误回应而非误导性的 not-found,见 src/runtime/builtin_bridge.zig)、dispatchDialogBridgeCommand(对话框三命令)分别路由到对应的 JSON 解析与平台服务调用。
显式启用内置命令
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" } }, }, },配置要点:
builtin_bridge.enabled = true是总开关;每条commands条目仍要单独列出命令名、所需权限与允许的 origin;- 窗口/子 WebView 类命令通常要求
"window"权限(当应用配置了权限列表时,见 src/runtime/builtin_bridge.zig 的allowsBuiltinBridgeCommand); - 对话框命令始终默认拒绝,必须显式列出;
js_window_api = true会额外暴露window.zero.windows.*与window.zero.webviews.*这两个便捷 API,但它不会绕过 origin 或权限检查——所有调用仍走同一套策略判定。
从 JavaScript 管理窗口
启用js_window_api与相应builtin_bridge策略后,前端可以直接创建、枚举、聚焦与关闭原生窗口:
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);create返回的win.id是窗口句柄;windows.list()返回所有窗口信息。窗口的创建参数在 packages/native-sdk/native-sdk.d.ts 中有完整类型定义:除label/title/width/height/x/y外,还支持restoreState(默认true)、titlebar(standard/hidden_inset/hidden_inset_tall/chromeless)、transparent、alwaysOnTop、clickThrough、activateOnShow与可选的url初始源。窗口选择的底层实现支持按id或按label解析(resolveWindowSelector,见 src/runtime/builtin_bridge.zig)。
窗口状态持久化依赖稳定的 label:请为窗口使用有意义的标签,如main、settings、tools、preview,这样窗口的几何状态、显示/隐藏策略才能在重启后稳定恢复(restore_state)。
分层 WebViews(Layered WebViews)
子 WebView 是叠放在原生窗口内部的原生 WebView,适合做预览面板、浏览器风格标签页、辅助工具栏等。前端创建与操控示例:
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();create的完整选项见 packages/native-sdk/native-sdk.d.ts:label(默认"webview")、windowId(父窗口 id,默认即调用方窗口)、url(其 origin 必须通过运行时导航策略)、frame(相对父窗口的逻辑坐标)、layer(原生 z 序,越大越靠上)、transparent(尽力透明背景)与bridge(是否注入window.zero)。返回的句柄带有setFrame/navigate/setZoom/setLayer/close方法(packages/native-sdk/native-sdk.d.ts);setZoom的合法范围为0.25到5.0(源码中越界返回InvalidWebViewOptions,见 src/runtime/builtin_bridge.zig)。
分层 WebView 规则:
- WebView 的 URL 必须通过导航策略(其 origin 须在
security.navigation.allowed_origins内),源码在createWebViewFromJson中通过validateWebViewUrl校验(src/runtime/builtin_bridge.zig); - 命令只作用于调用方所在的原生窗口:
windowId默认解析为调用窗口,传入时必须一致; main标签保留给启动 WebView,子 WebView 不得使用(validateChildWebViewLabel会拒绝main,见 src/runtime/validation.zig),main也不能被navigate/close操作;- 子 WebView只有以
bridge: true创建时才接收window.zero——默认false,这防止了不受信任内容获得原生能力; - 后端缺口(如平台不支持某操作)应以
invalid_request拒绝,而不是静默成功。
对话框:原生文件选择与消息框
对话框属于高风险原生能力,必须显式配置builtin_bridge策略后才能调用。三个内置命令对应三种场景:
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", });参数说明:
openFile:title、defaultPath(起始目录)、allowMultiple(多选)、allowDirectories(是否允许选目录)与可选的filters(文件类型过滤);saveFile:title、defaultName(默认文件名)与defaultPath;showMessage:style(如"warning")、title、message、primaryButton/secondaryButton按钮文本。
这些选项在进入平台层前都会经过严格校验(字段长度上限、禁止空字符等,见 src/runtime/validation.zig 的validateOpenDialogOptions/validateSaveDialogOptions/validateMessageDialogOptions),对话框命令本身则由dispatchDialogBridgeCommand路由到系统服务(src/runtime/builtin_bridge.zig)。
安全准则:只对可信的应用 UI 使用原生对话框;绝不要向远程或不受信任的 origin 暴露任意的文件系统访问能力。
错误处理:统一错误码
所有桥接调用失败时,Promise 都会 reject,错误对象带error.code与error.message:
| 错误码 | 触发场景 |
|---|---|
invalid_request | 输入畸形、不支持的操作、导航 URL 被拒、目标缺失、重复/保留标签 |
unknown_command | 没有注册对应处理器 |
permission_denied | origin 或权限检查失败 |
handler_failed | 处理器返回错误(或返回了非法 JSON) |
payload_too_large | 请求超过大小限制 |
internal_error | 运行时意外故障 |
错误码枚举定义于 src/bridge/root.zig,对应的 TypeScript 类型为NativeSdkErrorCode(packages/native-sdk/native-sdk.d.ts)。注意判定的优先级:策略拒绝先于命令未注册——源码测试"dispatcher reports permission denial before unknown command"明确验证了未启用策略的native.ping返回permission_denied而非unknown_command(src/bridge/root.zig)。
前端代码必须始终处理错误:
try { await window.zero.invoke("native.save", payload); } catch (error) { console.error(error.code, error.message); }不要只处理成功路径;尤其是用户可控输入或来自远程页面的调用,务必捕获并区分permission_denied(策略问题)与invalid_request(参数问题)以便定位。
源码佐证与验证手段
- 桥接核心:src/bridge/root.zig 包含
Dispatcher、Policy、Registry、parseRequest、响应序列化与全套单元测试(请求解析、畸形/超限拒绝、策略与 origin 校验、非法结果 JSON 拒绝等),是理解整条链路的权威入口; - 安全策略:src/security/root.zig 定义权限常量、
NavigationPolicy、ExternalLinkPolicy与hasPermission/allowsOrigin/allowsExternalUrl判定函数及测试; - 内置命令分发:src/runtime/builtin_bridge.zig 是 window/webview/view/dialog/os/credentials/clipboard/command 各内置命令的 JSON 解析与平台服务桥接层;
- 输入校验:src/runtime/validation.zig 覆盖命令名、标签、对话框选项、通知、凭据等字段的长度与字符校验;
- JS 类型契约:packages/native-sdk/native-sdk.d.ts 提供
window.zero相关 API 的完整 TypeScript 定义(NativeSdkWindowInfo、NativeSdkWebViewHandle、NativeSdkInvokeError等); - 清单 Schema:packages/native-sdk/schemas/app.schema.json 定义了
app.json中bridge、security、permissions、capabilities、windows等字段的合法取值与默认值。
综合来看,Native SDK 的桥接体系以"解析 → 限流 → 鉴权 → 路由 → 执行 → 校验响应"的流水线为基础,以"默认拒绝、精确放行"为安全内核;应用自定义命令与内置命令(窗口、WebView、对话框)共用同一套 origin/permission 校验,只是启用入口不同。遵循本文的配置示例与大小、标签、origin 约束,即可在获得原生窗口、分层 WebView 与原生对话框能力的同时,守住前端与原生边界的安全底线。
- 桌面应用
- 跨平台
【免费下载链接】native
Toolkit for building native desktop apps
相关推荐
Native SDK安全架构完全指南:权限、能力与桥接策略的桌面应用加固清单
Native SDK安全架构完全指南:权限、能力与桥接策略的桌面应用加固清单 Native SDK(ze/native)是一个用原生引擎绘制界面、不依赖浏览器和
桌面应用跨平台Native SDK Canvas Preview:用 Zig 在单窗口内同时托管原生 Canvas 与平台 Webview 的实战指南
Native SDK Canvas Preview:用 Zig 在单窗口内同时托管原生 Canvas 与平台 Webview 的实战指南 导读 本指南围绕 ex
桌面应用跨平台Native SDK Capabilities 示例全解析:在受信任 WebView 中安全调用 macOS 系统能力
Native SDK Capabilities 示例全解析:在受信任 WebView 中安全调用 macOS 系统能力 本指南以仓库中 examples/cap
桌面应用跨平台
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考