@zogien/native-sdk 0.11.0-fork.2 → 0.11.0-fork.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +47 -23
  3. package/build/cli.zig +22 -0
  4. package/native-sdk.d.ts +2 -0
  5. package/package.json +10 -10
  6. package/packages/core/package-lock.json +2 -2
  7. package/packages/core/package.json +1 -1
  8. package/schemas/app.schema.json +9 -1
  9. package/skill-data/ts-core/SKILL.md +2 -0
  10. package/src/app_runner/app_assets.zig +85 -1
  11. package/src/app_runner/root.zig +63 -3
  12. package/src/app_runner/ts_core_main.zig +8 -0
  13. package/src/platform/macos/appkit_host.h +9 -0
  14. package/src/platform/macos/appkit_host.m +366 -0
  15. package/src/platform/macos/root.zig +29 -6
  16. package/src/platform/null_platform.zig +66 -0
  17. package/src/platform/root.zig +1 -0
  18. package/src/platform/types.zig +46 -0
  19. package/src/platform/windows/root.zig +174 -0
  20. package/src/platform/windows/scriptc_mingw_compat.c +248 -0
  21. package/src/platform/windows/webview2_host.cpp +500 -26
  22. package/src/primitives/app_manifest/root.zig +1 -0
  23. package/src/primitives/app_manifest/tests.zig +35 -0
  24. package/src/primitives/app_manifest/types.zig +12 -0
  25. package/src/primitives/app_manifest/validation.zig +44 -6
  26. package/src/root.zig +1 -0
  27. package/src/runtime/bridge_responses.zig +2 -0
  28. package/src/runtime/channel_sources.zig +49 -1
  29. package/src/runtime/core.zig +5 -0
  30. package/src/runtime/effects.zig +12 -1
  31. package/src/runtime/flow.zig +6 -0
  32. package/src/runtime/flow_reference.zig +6 -0
  33. package/src/runtime/root.zig +4 -0
  34. package/src/runtime/single_instance.zig +893 -0
  35. package/src/runtime/tests.zig +1 -0
  36. package/src/runtime/window_controls.zig +73 -0
  37. package/src/runtime/window_controls_tests.zig +192 -0
  38. package/src/runtime/window_storage.zig +3 -0
  39. package/src/tooling/manifest.zig +111 -3
  40. package/src/tooling/package.zig +98 -8
  41. package/src/tooling/raw_manifest.zig +7 -0
  42. package/src/tooling/root.zig +2 -0
  43. package/src/tooling/templates.zig +53 -3
  44. package/src/tooling/update.zig +228 -10
  45. package/src/tooling/windows_installer.zig +245 -0
  46. package/src/tooling/zip_writer.zig +211 -0
  47. package/src/updater/engine.zig +936 -0
  48. package/src/updater/feed.zig +261 -12
  49. package/src/updater/macos.zig +187 -0
  50. package/src/updater/root.zig +41 -0
  51. package/src/updater/windows.zig +470 -0
  52. package/src/windows_install/config.zig +247 -0
  53. package/src/windows_install/main.zig +690 -0
  54. package/src/windows_install/payload.zig +146 -0
  55. package/src/windows_install/switch.zig +184 -0
  56. package/src/windows_install/tests.zig +11 -0
  57. package/src/windows_install/win32.zig +248 -0
  58. package/tools/native-sdk/main.zig +10 -9
@@ -21,6 +21,8 @@ pub const android = @import("android.zig");
21
21
  pub const xcodeproj = @import("xcodeproj.zig");
22
22
  pub const db = @import("db.zig");
23
23
  pub const update = @import("update.zig");
24
+ pub const zip_writer = @import("zip_writer.zig");
25
+ pub const windows_installer = @import("windows_installer.zig");
24
26
 
25
27
  test {
26
28
  @import("std").testing.refAllDecls(@This());
@@ -1493,9 +1493,12 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path:
1493
1493
  \\ // release-shaped exes are GUI-subsystem so the app never flashes a
1494
1494
  \\ // console behind its window; Debug keeps the console for dev logs.
1495
1495
  \\ // Redirected logging still works on GUI exes - only console
1496
- \\ // AUTO-allocation is subsystem-gated.
1496
+ \\ // AUTO-allocation is subsystem-gated. MSVC's CRT enters a GUI exe
1497
+ \\ // through WinMain, which Zig apps do not export, so MSVC targets
1498
+ \\ // keep the GUI subsystem but enter through mainCRTStartup.
1497
1499
  \\ if (target.result.os.tag == .windows and optimize != .Debug) {
1498
1500
  \\ exe.subsystem = .windows;
1501
+ \\ if (target.result.abi == .msvc) exe.entry = .{ .symbol_name = "mainCRTStartup" };
1499
1502
  \\ }
1500
1503
  \\ linkPlatform(b, target, app_mod, exe, selected_platform, web_engine, web_layer, native_sdk_path, cef_dir, cef_auto_install);
1501
1504
  \\ b.installArtifact(exe);
@@ -1554,6 +1557,7 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path:
1554
1557
  \\ // exe's own mode: release-shaped Windows exes are GUI-subsystem.
1555
1558
  \\ if (target.result.os.tag == .windows and package_optimize != .Debug) {
1556
1559
  \\ built.subsystem = .windows;
1560
+ \\ if (target.result.abi == .msvc) built.entry = .{ .symbol_name = "mainCRTStartup" };
1557
1561
  \\ }
1558
1562
  \\ linkPlatform(b, target, package_app_mod, built, selected_platform, web_engine, web_layer, native_sdk_path, cef_dir, cef_auto_install);
1559
1563
  \\ break :pkg built;
@@ -1593,6 +1597,9 @@ fn buildZig(allocator: std.mem.Allocator, names: TemplateNames, framework_path:
1593
1597
  \\ package.addArgs(&.{ "--web-layer", if (web_layer) "include" else "exclude" });
1594
1598
  \\ if (cef_auto_install) package.addArg("--cef-auto-install");
1595
1599
  \\ if (app_config.updates_enabled and package_target == .macos and b.graph.host.result.os.tag == .macos) package.addArg("--update-archive");
1600
+ \\ // Windows updates reach only installed apps, so an app with updates
1601
+ \\ // ships the per-user setup program beside the archive its feed names.
1602
+ \\ if (app_config.updates_enabled and package_target == .windows) package.addArgs(&.{ "--installer", "--update-archive" });
1596
1603
  \\ package.step.dependOn(&package_exe.step);
1597
1604
  \\ package.step.dependOn(&frontend_build.step);
1598
1605
  \\ const package_step = b.step("package", "Create a local package artifact");
@@ -2436,8 +2443,9 @@ fn runnerZig() []const u8 {
2436
2443
  \\ .bundle_id = self.bundle_id,
2437
2444
  \\ .icon_path = self.icon_path,
2438
2445
  \\ .update_feed_url = manifestUpdateString("feed_url"),
2439
- \\ .update_public_key = manifestUpdateString("public_key"),
2446
+ \\ .update_public_key = manifestUpdatePublicKeys(),
2440
2447
  \\ .update_check_on_start = manifestUpdateCheckOnStart(),
2448
+ \\ .update_mode = manifestUpdateMode(),
2441
2449
  \\ };
2442
2450
  \\ const windows = manifestWindowOptions(buffers);
2443
2451
  \\ if (windows.len > 0) {
@@ -2514,6 +2522,36 @@ fn runnerZig() []const u8 {
2514
2522
  \\ return app_manifest.updates.check_on_start;
2515
2523
  \\}
2516
2524
  \\
2525
+ \\/// The trusted update keys the updater verifies feeds against:
2526
+ \\/// `public_key` followed by any `public_keys`, comma-joined.
2527
+ \\fn manifestUpdatePublicKeys() []const u8 {
2528
+ \\ const primary = comptime manifestUpdateString("public_key");
2529
+ \\ if (comptime primary.len == 0) return "";
2530
+ \\ if (comptime !@hasField(@TypeOf(app_manifest.updates), "public_keys")) return primary;
2531
+ \\ comptime var keys: []const u8 = primary;
2532
+ \\ inline for (app_manifest.updates.public_keys) |key| keys = keys ++ "," ++ key;
2533
+ \\ return keys;
2534
+ \\}
2535
+ \\
2536
+ \\fn manifestUpdateMode() native_sdk.platform.UpdateMode {
2537
+ \\ if (comptime !@hasField(@TypeOf(app_manifest), "updates")) return .host;
2538
+ \\ if (comptime !@hasField(@TypeOf(app_manifest.updates), "mode")) return .host;
2539
+ \\ return comptime std.meta.stringToEnum(native_sdk.platform.UpdateMode, app_manifest.updates.mode) orelse
2540
+ \\ @compileError("app manifest updates.mode must be \"host\" or \"app\"");
2541
+ \\}
2542
+ \\
2543
+ \\/// Points the app-driven updater at this installation (see
2544
+ \\/// `updater.configureApp`).
2545
+ \\fn configureUpdates(app_info: *native_sdk.platform.AppInfo, environ_map: *std.process.Environ.Map) void {
2546
+ \\ const host: native_sdk.updater.Host = comptime if (std.mem.eql(u8, build_options.platform, "windows"))
2547
+ \\ .windows
2548
+ \\ else if (std.mem.eql(u8, build_options.platform, "macos") and std.mem.eql(u8, build_options.web_engine, "system"))
2549
+ \\ .macos_appkit
2550
+ \\ else
2551
+ \\ .other;
2552
+ \\ native_sdk.updater.configureApp(host, app_info, environ_map);
2553
+ \\}
2554
+ \\
2517
2555
  \\const CommandStorage = struct {
2518
2556
  \\ commands: [native_sdk.app_manifest.max_commands]native_sdk.Command = undefined,
2519
2557
  \\
@@ -3058,6 +3096,7 @@ fn runnerZig() []const u8 {
3058
3096
  \\fn runMacos(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
3059
3097
  \\ var buffers: StateBuffers = undefined;
3060
3098
  \\ var app_info = options.appInfo(&buffers);
3099
+ \\ configureUpdates(&app_info, init.environ_map);
3061
3100
  \\ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
3062
3101
  \\ // Heap wrapper, latch-gated free: worker threads hold this address
3063
3102
  \\ // as the channel wake context and an abandoned wake call may
@@ -3116,6 +3155,7 @@ fn runnerZig() []const u8 {
3116
3155
  \\fn runLinux(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
3117
3156
  \\ var buffers: StateBuffers = undefined;
3118
3157
  \\ var app_info = options.appInfo(&buffers);
3158
+ \\ configureUpdates(&app_info, init.environ_map);
3119
3159
  \\ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
3120
3160
  \\ // Heap wrapper, latch-gated free: worker threads hold this address
3121
3161
  \\ // as the channel wake context and an abandoned wake call may
@@ -3174,6 +3214,7 @@ fn runnerZig() []const u8 {
3174
3214
  \\fn runWindows(app: native_sdk.App, options: RunOptions, init: std.process.Init) !void {
3175
3215
  \\ var buffers: StateBuffers = undefined;
3176
3216
  \\ var app_info = options.appInfo(&buffers);
3217
+ \\ configureUpdates(&app_info, init.environ_map);
3177
3218
  \\ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
3178
3219
  \\ // Heap wrapper, latch-gated free: worker threads hold this address
3179
3220
  \\ // as the channel wake context and an abandoned wake call may
@@ -4268,6 +4309,7 @@ test "writeDefaultApp emits Vite project files" {
4268
4309
  // the packaged artifact structurally agrees with the compiled exe.
4269
4310
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package.addArgs(&.{ \"--web-layer\", if (web_layer) \"include\" else \"exclude\" })") != null);
4270
4311
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package.addArg(\"--update-archive\")") != null);
4312
+ try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package_target == .windows) package.addArgs(&.{ \"--installer\", \"--update-archive\" })") != null);
4271
4313
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "options.addOption(bool, \"web_layer\", web_layer)") != null);
4272
4314
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "std.json.parseFromSliceLeaky(InferenceManifest") != null);
4273
4315
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "fn appManifestModule") != null);
@@ -4284,6 +4326,9 @@ test "writeDefaultApp emits Vite project files" {
4284
4326
  // the SDK build graph) so packaged scaffold apps never flash a console.
4285
4327
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "if (target.result.os.tag == .windows and optimize != .Debug) {") != null);
4286
4328
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "exe.subsystem = .windows;") != null);
4329
+ // MSVC's CRT would enter that GUI exe through WinMain, which Zig apps
4330
+ // do not export; MSVC targets enter through mainCRTStartup instead.
4331
+ try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "if (target.result.abi == .msvc) exe.entry = .{ .symbol_name = \"mainCRTStartup\" };") != null);
4287
4332
  // The package step wraps its own release-shaped exe: Debug stays the
4288
4333
  // run/dev default, but `zig build package` must never ship a Debug
4289
4334
  // (console-subsystem) binary unless the user pinned the mode
@@ -4296,6 +4341,7 @@ test "writeDefaultApp emits Vite project files" {
4296
4341
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "const package_exe = if (package_optimize == optimize) exe else pkg: {") != null);
4297
4342
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "if (target.result.os.tag == .windows and package_optimize != .Debug) {") != null);
4298
4343
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "built.subsystem = .windows;") != null);
4344
+ try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "if (target.result.abi == .msvc) built.entry = .{ .symbol_name = \"mainCRTStartup\" };") != null);
4299
4345
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package.addFileArg(package_exe.getEmittedBin());") != null);
4300
4346
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package.step.dependOn(&package_exe.step);") != null);
4301
4347
  try std.testing.expect(std.mem.indexOf(u8, build_zig_text, "package_optimize_name,") != null);
@@ -4328,8 +4374,12 @@ test "writeDefaultApp emits Vite project files" {
4328
4374
  try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, "fn appInfo(self: RunOptions, buffers: *StateBuffers)") != null);
4329
4375
  try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".version = manifestStringField(\"version\")") != null);
4330
4376
  try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_feed_url = manifestUpdateString(\"feed_url\")") != null);
4331
- try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_public_key = manifestUpdateString(\"public_key\")") != null);
4377
+ try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_public_key = manifestUpdatePublicKeys()") != null);
4332
4378
  try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_check_on_start = manifestUpdateCheckOnStart()") != null);
4379
+ try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, ".update_mode = manifestUpdateMode()") != null);
4380
+ // Ejected apps configure the app-driven updater on every desktop
4381
+ // platform.
4382
+ try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, runner_zig_text, "configureUpdates(&app_info, init.environ_map);"));
4333
4383
  try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, "fn manifestWindowOptions") != null);
4334
4384
  try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, "info.windows = windows") != null);
4335
4385
  try std.testing.expect(std.mem.indexOf(u8, runner_zig_text, "for (restored_windows, 0..)") != null);
@@ -1,6 +1,7 @@
1
1
  const std = @import("std");
2
2
  const manifest_tool = @import("manifest.zig");
3
3
  const update_feed = @import("update_feed");
4
+ const install_config = @import("windows_install_config");
4
5
 
5
6
  const Ed25519 = std.crypto.sign.Ed25519;
6
7
 
@@ -50,9 +51,14 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
50
51
  var target: ?[]const u8 = null;
51
52
  var release_notes: []const u8 = "";
52
53
  var output_path: []const u8 = "native-update.json";
54
+ var url_policy: update_feed.UrlPolicy = .{};
53
55
  var index: usize = 0;
54
56
  while (index < args.len) : (index += 1) {
55
57
  const name = args[index];
58
+ if (std.mem.eql(u8, name, "--allow-loopback-http")) {
59
+ url_policy.allow_loopback_http = true;
60
+ continue;
61
+ }
56
62
  index += 1;
57
63
  if (index >= args.len) return error.InvalidArguments;
58
64
  const value = args[index];
@@ -61,15 +67,16 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
61
67
  const archive = archive_path orelse return error.InvalidArguments;
62
68
  const url = archive_url orelse return error.InvalidArguments;
63
69
  const release_target = target orelse return error.InvalidArguments;
64
- if (!update_feed.archiveUrlIsValid(url)) return error.InvalidArchiveUrl;
65
- if (!std.mem.eql(u8, release_target, "macos-aarch64") and !std.mem.eql(u8, release_target, "macos-x86_64")) return error.InvalidTarget;
70
+ if (!update_feed.urlAllowed(url, url_policy)) return error.InvalidArchiveUrl;
71
+ if (!update_feed.isTarget(release_target)) return error.InvalidTarget;
66
72
  if (!std.mem.endsWith(u8, archive, ".zip")) return error.InvalidArchive;
67
73
  const metadata = try manifest_tool.readMetadata(allocator, io, manifest_path orelse manifest_tool.defaultPath(io) orelse "app.json");
68
74
  defer metadata.deinit(allocator);
69
75
  if (!metadata.updates.enabled()) return error.UpdatesNotConfigured;
70
76
  _ = manifest_tool.parseVersion(metadata.version) catch return error.InvalidManifestVersion;
71
77
  if (release_notes.len > 16 * 1024) return error.ReleaseNotesTooLarge;
72
- validateUpdateArchive(allocator, io, archive, metadata.id, metadata.version, metadata.name, release_target) catch |err| switch (err) {
78
+ const validate = if (std.mem.startsWith(u8, release_target, "windows-")) &validateWindowsUpdateArchive else &validateUpdateArchive;
79
+ validate(allocator, io, archive, metadata.id, metadata.version, metadata.name, release_target) catch |err| switch (err) {
73
80
  error.UpdateArchitectureMismatch => return error.UpdateArchitectureMismatch,
74
81
  else => return error.InvalidArchive,
75
82
  };
@@ -83,12 +90,14 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
83
90
  var extra: [1]u8 = undefined;
84
91
  if (try key_reader.interface.readSliceShort(&extra) != 0) return error.InvalidPrivateKey;
85
92
  const key_pair = try Ed25519.KeyPair.generateDeterministic(seed);
86
- const configured_public_key = metadata.updates.public_key orelse return error.UpdatesNotConfigured;
87
- var configured_public_key_bytes: [Ed25519.PublicKey.encoded_length]u8 = undefined;
88
- const configured_public_key_size = std.base64.standard.Decoder.calcSizeForSlice(configured_public_key) catch return error.InvalidUpdatePublicKey;
89
- if (configured_public_key_size != configured_public_key_bytes.len) return error.InvalidUpdatePublicKey;
90
- std.base64.standard.Decoder.decode(&configured_public_key_bytes, configured_public_key) catch return error.InvalidUpdatePublicKey;
91
- if (!std.crypto.timing_safe.eql([Ed25519.PublicKey.encoded_length]u8, key_pair.public_key.toBytes(), configured_public_key_bytes)) return error.UpdateKeyMismatch;
93
+ // The release's installs trust the manifest's `public_key` and any
94
+ // `public_keys`, so a release that rotates keys is signed with the key
95
+ // the installs it replaces already know.
96
+ if (metadata.updates.public_key == null) return error.UpdatesNotConfigured;
97
+ if (!metadata.updates.publicKeysAreValid()) return error.InvalidUpdatePublicKey;
98
+ var signing_public_key_base64: [std.base64.standard.Encoder.calcSize(Ed25519.PublicKey.encoded_length)]u8 = undefined;
99
+ const signing_public_key = std.base64.standard.Encoder.encode(&signing_public_key_base64, &key_pair.public_key.toBytes());
100
+ if (!metadata.updates.trustsPublicKey(signing_public_key)) return error.UpdateKeyMismatch;
92
101
 
93
102
  var archive_file = try std.Io.Dir.cwd().openFile(io, archive, .{});
94
103
  defer archive_file.close(io);
@@ -139,7 +148,7 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
139
148
  defer output_writer.deinit();
140
149
  try std.json.Stringify.value(.{ .payload = payload_base64, .signature = &signature_base64 }, .{ .whitespace = .indent_2 }, &output_writer.writer);
141
150
  try output_writer.writer.writeByte('\n');
142
- var verified = try update_feed.verifyEnvelope(allocator, output_writer.written(), configured_public_key);
151
+ var verified = try update_feed.verifyEnvelopeWithPolicy(allocator, output_writer.written(), signing_public_key, url_policy);
143
152
  defer verified.deinit(allocator);
144
153
  if (!std.mem.eql(u8, verified.release.bundle_id, metadata.id) or
145
154
  !std.mem.eql(u8, verified.release.version, metadata.version) or
@@ -149,6 +158,9 @@ fn sign(allocator: std.mem.Allocator, io: std.Io, args: []const []const u8) !voi
149
158
  try atomic.file.writeStreamingAll(io, output_writer.written());
150
159
  try atomic.replace(io);
151
160
  std.debug.print("signed {s} {s} update ({d} bytes) into {s}\n", .{ metadata.displayName(), metadata.version, archive_stat.size, output_path });
161
+ if (!update_feed.archiveUrlIsValid(url)) {
162
+ std.debug.print("note: the archive URL is loopback http://; installed apps accept this feed only while NATIVE_SDK_UPDATES_FEED_URL points them at it\n", .{});
163
+ }
152
164
  }
153
165
 
154
166
  const max_info_plist_bytes: u64 = 1024 * 1024;
@@ -218,6 +230,72 @@ fn validateUpdateArchive(
218
230
  !plistStringValueEquals(plist, "CFBundlePackageType", "APPL")) return error.InvalidArchive;
219
231
  }
220
232
 
233
+ /// A Windows update archive is the install image `native package
234
+ /// --update-archive` writes: the app's install configuration naming this
235
+ /// app and version, its launcher, and `bin\<name>.exe` built for the
236
+ /// target architecture.
237
+ fn validateWindowsUpdateArchive(
238
+ allocator: std.mem.Allocator,
239
+ io: std.Io,
240
+ archive_path: []const u8,
241
+ expected_bundle_id: []const u8,
242
+ expected_version: []const u8,
243
+ expected_name: []const u8,
244
+ expected_target: []const u8,
245
+ ) !void {
246
+ const expected_machine: u16 = if (std.mem.eql(u8, expected_target, "windows-aarch64")) 0xaa64 else if (std.mem.eql(u8, expected_target, "windows-x86_64")) 0x8664 else return error.InvalidTarget;
247
+ var file = try std.Io.Dir.cwd().openFile(io, archive_path, .{});
248
+ defer file.close(io);
249
+ var reader_buffer: [64 * 1024]u8 = undefined;
250
+ var reader = file.reader(io, &reader_buffer);
251
+ var iterator = try std.zip.Iterator.init(&reader);
252
+ var filename_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined;
253
+ var executable_name_buffer: [256]u8 = undefined;
254
+ const executable_name = try std.fmt.bufPrint(&executable_name_buffer, "bin/{s}.exe", .{expected_name});
255
+ var config_bytes: ?[]u8 = null;
256
+ defer if (config_bytes) |bytes| allocator.free(bytes);
257
+ var has_launcher = false;
258
+ var executable_machine: ?u16 = null;
259
+ var entries: usize = 0;
260
+
261
+ while (try iterator.next()) |entry| {
262
+ const filename = try zipEntryFilename(&reader, entry, &filename_buffer);
263
+ if (!zipEntryPathIsSafe(filename)) return error.InvalidArchive;
264
+ // Installed apps unpack with std.zip, which reads only these.
265
+ if (entry.compression_method != .store and entry.compression_method != .deflate) return error.InvalidArchive;
266
+ entries += 1;
267
+ if (entries > 1_000_000) return error.InvalidArchive;
268
+ if (std.mem.eql(u8, filename, install_config.file_name)) {
269
+ if (config_bytes != null) return error.InvalidArchive;
270
+ config_bytes = try readZipEntryAlloc(allocator, &reader, entry, filename, install_config.max_config_bytes);
271
+ } else if (std.mem.eql(u8, filename, "native-launcher.exe")) {
272
+ has_launcher = true;
273
+ } else if (std.ascii.eqlIgnoreCase(filename, executable_name)) {
274
+ var header: [4096]u8 = undefined;
275
+ const prefix = try readZipEntryPrefix(&reader, entry, filename, &header);
276
+ executable_machine = peMachine(prefix) orelse return error.InvalidArchive;
277
+ }
278
+ }
279
+
280
+ const bytes = config_bytes orelse return error.InvalidArchive;
281
+ var parsed = install_config.parse(allocator, bytes) catch return error.InvalidArchive;
282
+ defer parsed.deinit();
283
+ const config = parsed.value;
284
+ if (!std.mem.eql(u8, config.bundle_id, expected_bundle_id) or
285
+ !std.mem.eql(u8, config.version, expected_version) or
286
+ !std.mem.eql(u8, config.name, expected_name)) return error.InvalidArchive;
287
+ if (!has_launcher) return error.InvalidArchive;
288
+ const machine = executable_machine orelse return error.InvalidArchive;
289
+ if (machine != expected_machine) return error.UpdateArchitectureMismatch;
290
+ }
291
+
292
+ fn peMachine(header: []const u8) ?u16 {
293
+ if (header.len < 0x40 or header[0] != 'M' or header[1] != 'Z') return null;
294
+ const pe_offset: usize = std.mem.readInt(u32, header[0x3c..0x40], .little);
295
+ if (pe_offset + 6 > header.len or !std.mem.eql(u8, header[pe_offset..][0..4], "PE\x00\x00")) return null;
296
+ return std.mem.readInt(u16, header[pe_offset + 4 ..][0..2], .little);
297
+ }
298
+
221
299
  const max_macho_architectures: usize = 64;
222
300
  const max_macho_header_bytes: usize = 8 + max_macho_architectures * 32;
223
301
  const cpu_type_x86_64: u32 = 0x01000007;
@@ -574,3 +652,143 @@ test "update archive validation requires one matching app bundle" {
574
652
  try std.testing.expectError(error.InvalidArchive, validateUpdateArchive(std.testing.allocator, std.testing.io, valid_path, "com.example.other", "1.2.3", "demo", "macos-aarch64"));
575
653
  try std.testing.expectError(error.InvalidArchive, validateUpdateArchive(std.testing.allocator, std.testing.io, valid_path, "com.example.demo", "1.2.4", "demo", "macos-aarch64"));
576
654
  }
655
+
656
+ test "a release that rotates update keys is signed with a key its installs already trust" {
657
+ const allocator = std.testing.allocator;
658
+ const io = std.testing.io;
659
+ var tmp = std.testing.tmpDir(.{});
660
+ defer tmp.cleanup();
661
+ const root = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{tmp.sub_path});
662
+ defer allocator.free(root);
663
+
664
+ const old_key = try Ed25519.KeyPair.generateDeterministic([_]u8{0x51} ** Ed25519.KeyPair.seed_length);
665
+ const unrelated_key = try Ed25519.KeyPair.generateDeterministic([_]u8{0x53} ** Ed25519.KeyPair.seed_length);
666
+ const old_seed = old_key.secret_key.seed();
667
+ const unrelated_seed = unrelated_key.secret_key.seed();
668
+ try tmp.dir.writeFile(io, .{ .sub_path = "old.key", .data = &old_seed });
669
+ try tmp.dir.writeFile(io, .{ .sub_path = "unrelated.key", .data = &unrelated_seed });
670
+ // The new release names its new key as `public_key` and keeps the old
671
+ // one in `public_keys`; installs of the previous release know only
672
+ // the old one.
673
+ try tmp.dir.writeFile(io, .{ .sub_path = "app.json", .data =
674
+ \\{
675
+ \\ "id": "com.example.demo",
676
+ \\ "name": "demo",
677
+ \\ "version": "1.2.3",
678
+ \\ "updates": {
679
+ \\ "feed_url": "https://example.com/releases/{target}/native-update.json",
680
+ \\ "public_key": "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc=",
681
+ \\ "public_keys": ["wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo="]
682
+ \\ }
683
+ \\}
684
+ });
685
+ var plist_buffer: [1024]u8 = undefined;
686
+ const plist = try updateInfoPlist("com.example.demo", "1.2.3", "demo", &plist_buffer);
687
+ const executable = thinMachO64(cpu_type_arm64);
688
+ const archive_entries = [_]StoredZipEntry{
689
+ .{ .name = "Demo.app/Contents/Info.plist", .data = plist },
690
+ .{ .name = "Demo.app/Contents/MacOS/demo", .data = &executable },
691
+ };
692
+ const archive = try storedZipAlloc(allocator, &archive_entries);
693
+ defer allocator.free(archive);
694
+ try tmp.dir.writeFile(io, .{ .sub_path = "demo.zip", .data = archive });
695
+
696
+ const manifest_path = try std.fmt.allocPrint(allocator, "{s}/app.json", .{root});
697
+ defer allocator.free(manifest_path);
698
+ const archive_path = try std.fmt.allocPrint(allocator, "{s}/demo.zip", .{root});
699
+ defer allocator.free(archive_path);
700
+ const old_key_path = try std.fmt.allocPrint(allocator, "{s}/old.key", .{root});
701
+ defer allocator.free(old_key_path);
702
+ const unrelated_key_path = try std.fmt.allocPrint(allocator, "{s}/unrelated.key", .{root});
703
+ defer allocator.free(unrelated_key_path);
704
+ const output_path = try std.fmt.allocPrint(allocator, "{s}/native-update.json", .{root});
705
+ defer allocator.free(output_path);
706
+
707
+ try sign(allocator, io, &.{ "--private-key", old_key_path, "--manifest", manifest_path, "--archive", archive_path, "--url", "https://example.com/demo-1.2.3.zip", "--target", "macos-aarch64", "--output", output_path });
708
+ const feed = try std.Io.Dir.cwd().readFileAlloc(io, output_path, allocator, .limited(update_feed.max_envelope_bytes));
709
+ defer allocator.free(feed);
710
+ var verified = try update_feed.verifyEnvelope(allocator, feed, "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=");
711
+ defer verified.deinit(allocator);
712
+ try std.testing.expectEqualStrings("1.2.3", verified.release.version);
713
+ try std.testing.expectError(error.InvalidUpdateSignature, update_feed.verifyEnvelope(allocator, feed, "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc="));
714
+
715
+ try std.testing.expectError(error.UpdateKeyMismatch, sign(allocator, io, &.{ "--private-key", unrelated_key_path, "--manifest", manifest_path, "--archive", archive_path, "--url", "https://example.com/demo-1.2.3.zip", "--target", "macos-aarch64", "--output", output_path }));
716
+
717
+ // A key that does not decode is reported as such, not as a mismatch.
718
+ try tmp.dir.writeFile(io, .{ .sub_path = "app.json", .data =
719
+ \\{
720
+ \\ "id": "com.example.demo",
721
+ \\ "name": "demo",
722
+ \\ "version": "1.2.3",
723
+ \\ "updates": {
724
+ \\ "feed_url": "https://example.com/releases/{target}/native-update.json",
725
+ \\ "public_key": "IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc=",
726
+ \\ "public_keys": ["wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=", "not-a-key"]
727
+ \\ }
728
+ \\}
729
+ });
730
+ try std.testing.expectError(error.InvalidUpdatePublicKey, sign(allocator, io, &.{ "--private-key", old_key_path, "--manifest", manifest_path, "--archive", archive_path, "--url", "https://example.com/demo-1.2.3.zip", "--target", "macos-aarch64", "--output", output_path }));
731
+ }
732
+
733
+ test "a Windows release signs its installed image and names loopback http only when asked" {
734
+ const allocator = std.testing.allocator;
735
+ const io = std.testing.io;
736
+ var tmp = std.testing.tmpDir(.{});
737
+ defer tmp.cleanup();
738
+ const root = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}", .{tmp.sub_path});
739
+ defer allocator.free(root);
740
+
741
+ const key = try Ed25519.KeyPair.generateDeterministic([_]u8{0x51} ** Ed25519.KeyPair.seed_length);
742
+ const seed = key.secret_key.seed();
743
+ try tmp.dir.writeFile(io, .{ .sub_path = "release.key", .data = &seed });
744
+ try tmp.dir.writeFile(io, .{ .sub_path = "app.json", .data =
745
+ \\{
746
+ \\ "id": "com.example.demo",
747
+ \\ "name": "demo",
748
+ \\ "version": "1.2.3",
749
+ \\ "updates": {
750
+ \\ "feed_url": "https://example.com/releases/{target}/native-update.json",
751
+ \\ "public_key": "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo="
752
+ \\ }
753
+ \\}
754
+ });
755
+ var config_writer = std.Io.Writer.Allocating.init(allocator);
756
+ defer config_writer.deinit();
757
+ try install_config.write(.{ .bundle_id = "com.example.demo", .name = "demo", .display_name = "Demo", .version = "1.2.3", .executable = "demo.exe" }, &config_writer.writer);
758
+ var executable = [_]u8{0} ** 0x100;
759
+ executable[0] = 'M';
760
+ executable[1] = 'Z';
761
+ std.mem.writeInt(u32, executable[0x3c..0x40], 0x80, .little);
762
+ @memcpy(executable[0x80..0x84], "PE\x00\x00");
763
+ std.mem.writeInt(u16, executable[0x84..0x86], 0x8664, .little);
764
+ const archive = try storedZipAlloc(allocator, &.{
765
+ .{ .name = install_config.file_name, .data = config_writer.written() },
766
+ .{ .name = "native-launcher.exe", .data = "launcher" },
767
+ .{ .name = "bin/demo.exe", .data = &executable },
768
+ });
769
+ defer allocator.free(archive);
770
+ try tmp.dir.writeFile(io, .{ .sub_path = "demo.zip", .data = archive });
771
+
772
+ const manifest_path = try std.fmt.allocPrint(allocator, "{s}/app.json", .{root});
773
+ defer allocator.free(manifest_path);
774
+ const archive_path = try std.fmt.allocPrint(allocator, "{s}/demo.zip", .{root});
775
+ defer allocator.free(archive_path);
776
+ const key_path = try std.fmt.allocPrint(allocator, "{s}/release.key", .{root});
777
+ defer allocator.free(key_path);
778
+ const output_path = try std.fmt.allocPrint(allocator, "{s}/native-update.json", .{root});
779
+ defer allocator.free(output_path);
780
+ const common = [_][]const u8{ "--private-key", key_path, "--manifest", manifest_path, "--archive", archive_path, "--output", output_path };
781
+
782
+ try sign(allocator, io, &(common ++ [_][]const u8{ "--url", "https://example.com/demo-1.2.3.zip", "--target", "windows-x86_64" }));
783
+ try std.testing.expectError(error.UpdateArchitectureMismatch, sign(allocator, io, &(common ++ [_][]const u8{ "--url", "https://example.com/demo-1.2.3.zip", "--target", "windows-aarch64" })));
784
+
785
+ const local_url = "http://127.0.0.1:8080/demo-1.2.3.zip";
786
+ try std.testing.expectError(error.InvalidArchiveUrl, sign(allocator, io, &(common ++ [_][]const u8{ "--url", local_url, "--target", "windows-x86_64" })));
787
+ try sign(allocator, io, &(common ++ [_][]const u8{ "--url", local_url, "--target", "windows-x86_64", "--allow-loopback-http" }));
788
+ const feed = try std.Io.Dir.cwd().readFileAlloc(io, output_path, allocator, .limited(update_feed.max_envelope_bytes));
789
+ defer allocator.free(feed);
790
+ try std.testing.expectError(error.InvalidUpdatePayload, update_feed.verifyEnvelope(allocator, feed, "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo="));
791
+ var verified = try update_feed.verifyEnvelopeWithPolicy(allocator, feed, "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=", .{ .allow_loopback_http = true });
792
+ defer verified.deinit(allocator);
793
+ try std.testing.expectEqualStrings(local_url, verified.release.archive_url);
794
+ }