@zogien/native-sdk 0.11.0-fork.3 → 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 (49) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +3 -0
  3. package/build/cli.zig +22 -0
  4. package/package.json +10 -10
  5. package/packages/core/package-lock.json +2 -2
  6. package/packages/core/package.json +1 -1
  7. package/schemas/app.schema.json +9 -1
  8. package/skill-data/ts-core/SKILL.md +2 -0
  9. package/src/app_runner/app_assets.zig +85 -1
  10. package/src/app_runner/root.zig +63 -3
  11. package/src/app_runner/ts_core_main.zig +8 -0
  12. package/src/platform/macos/appkit_host.h +9 -0
  13. package/src/platform/macos/appkit_host.m +366 -0
  14. package/src/platform/macos/root.zig +29 -6
  15. package/src/platform/root.zig +1 -0
  16. package/src/platform/types.zig +9 -0
  17. package/src/platform/windows/root.zig +37 -0
  18. package/src/platform/windows/webview2_host.cpp +371 -26
  19. package/src/primitives/app_manifest/root.zig +1 -0
  20. package/src/primitives/app_manifest/tests.zig +35 -0
  21. package/src/primitives/app_manifest/types.zig +12 -0
  22. package/src/primitives/app_manifest/validation.zig +44 -6
  23. package/src/root.zig +1 -0
  24. package/src/runtime/channel_sources.zig +49 -1
  25. package/src/runtime/effects.zig +12 -1
  26. package/src/runtime/flow.zig +6 -0
  27. package/src/runtime/flow_reference.zig +6 -0
  28. package/src/runtime/root.zig +4 -0
  29. package/src/runtime/single_instance.zig +893 -0
  30. package/src/tooling/manifest.zig +111 -3
  31. package/src/tooling/package.zig +98 -8
  32. package/src/tooling/raw_manifest.zig +7 -0
  33. package/src/tooling/root.zig +2 -0
  34. package/src/tooling/templates.zig +44 -2
  35. package/src/tooling/update.zig +228 -10
  36. package/src/tooling/windows_installer.zig +245 -0
  37. package/src/tooling/zip_writer.zig +211 -0
  38. package/src/updater/engine.zig +936 -0
  39. package/src/updater/feed.zig +261 -12
  40. package/src/updater/macos.zig +187 -0
  41. package/src/updater/root.zig +41 -0
  42. package/src/updater/windows.zig +470 -0
  43. package/src/windows_install/config.zig +247 -0
  44. package/src/windows_install/main.zig +690 -0
  45. package/src/windows_install/payload.zig +146 -0
  46. package/src/windows_install/switch.zig +184 -0
  47. package/src/windows_install/tests.zig +11 -0
  48. package/src/windows_install/win32.zig +248 -0
  49. package/tools/native-sdk/main.zig +10 -9
@@ -657,17 +657,55 @@ pub fn validateSecurity(security: SecurityConfig) ValidationError!void {
657
657
  pub fn validateUpdates(updates: UpdateConfig) ValidationError!void {
658
658
  if ((updates.feed_url == null) != (updates.public_key == null)) return error.MissingRequiredField;
659
659
  if (updates.check_on_start and updates.feed_url == null) return error.MissingRequiredField;
660
+ if (updates.mode == .app and updates.feed_url == null) return error.MissingRequiredField;
661
+ if (updates.public_keys.len > 0 and updates.public_key == null) return error.MissingRequiredField;
662
+ if (updates.public_keys.len > types.max_update_public_keys) return error.MissingRequiredField;
660
663
  if (updates.feed_url) |url| {
661
664
  if (url.len > 4096) return error.InvalidUrl;
662
665
  if (!std.mem.startsWith(u8, url, "https://")) return error.InvalidUrl;
663
666
  try validateUrl(url);
667
+ try validateFeedUrlPlaceholders(url);
668
+ }
669
+ if (updates.public_key) |key| try validateUpdatePublicKey(key);
670
+ for (updates.public_keys, 0..) |key, index| {
671
+ try validateUpdatePublicKey(key);
672
+ if (std.mem.eql(u8, key, updates.public_key.?)) return error.MissingRequiredField;
673
+ for (updates.public_keys[0..index]) |previous| {
674
+ if (std.mem.eql(u8, key, previous)) return error.MissingRequiredField;
675
+ }
664
676
  }
665
- if (updates.public_key) |key| {
666
- var decoded: [std.crypto.sign.Ed25519.PublicKey.encoded_length]u8 = undefined;
667
- const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(key) catch return error.MissingRequiredField;
668
- if (decoded_len != decoded.len) return error.MissingRequiredField;
669
- std.base64.standard.Decoder.decode(&decoded, key) catch return error.MissingRequiredField;
670
- _ = std.crypto.sign.Ed25519.PublicKey.fromBytes(decoded) catch return error.MissingRequiredField;
677
+ }
678
+
679
+ fn validateUpdatePublicKey(key: []const u8) ValidationError!void {
680
+ var decoded: [std.crypto.sign.Ed25519.PublicKey.encoded_length]u8 = undefined;
681
+ const decoded_len = std.base64.standard.Decoder.calcSizeForSlice(key) catch return error.MissingRequiredField;
682
+ if (decoded_len != decoded.len) return error.MissingRequiredField;
683
+ std.base64.standard.Decoder.decode(&decoded, key) catch return error.MissingRequiredField;
684
+ _ = std.crypto.sign.Ed25519.PublicKey.fromBytes(decoded) catch return error.MissingRequiredField;
685
+ }
686
+
687
+ /// A feed_url may name `{target}` and `{current_version}`, which each
688
+ /// installed app expands for itself, in its path or query only.
689
+ fn validateFeedUrlPlaceholders(url: []const u8) ValidationError!void {
690
+ const authority_start = "https://".len;
691
+ const authority_end = std.mem.findAnyPos(u8, url, authority_start, "/?#") orelse url.len;
692
+ if (std.mem.findAny(u8, url[0..authority_end], "{}") != null) return error.InvalidUrl;
693
+ var index = authority_end;
694
+ while (index < url.len) : (index += 1) {
695
+ switch (url[index]) {
696
+ '{' => {
697
+ const rest = url[index..];
698
+ const placeholder = if (std.mem.startsWith(u8, rest, "{target}"))
699
+ "{target}"
700
+ else if (std.mem.startsWith(u8, rest, "{current_version}"))
701
+ "{current_version}"
702
+ else
703
+ return error.InvalidUrl;
704
+ index += placeholder.len - 1;
705
+ },
706
+ '}' => return error.InvalidUrl,
707
+ else => {},
708
+ }
671
709
  }
672
710
  }
673
711
 
package/src/root.zig CHANGED
@@ -157,6 +157,7 @@ pub const EffectChannelSourceState = runtime.EffectChannelSourceState;
157
157
  pub const EffectChannelSourceResult = runtime.EffectChannelSourceResult;
158
158
  pub const max_channel_source_payload = runtime.max_channel_source_payload;
159
159
  pub const ChannelHandle = runtime.ChannelHandle;
160
+ pub const single_instance = runtime.single_instance;
160
161
  pub const max_effect_channels = runtime.max_effect_channels;
161
162
  pub const max_effect_channel_bytes = runtime.max_effect_channel_bytes;
162
163
  pub const max_effect_channel_pending = runtime.max_effect_channel_pending;
@@ -1,19 +1,67 @@
1
1
  //! Explicit native external-producer capabilities. Portable app state and
2
- //! channel lifecycle stay in the app; this source owns a detached OS sampler.
2
+ //! channel lifecycle stay in the app; `native-sdk.process.samples` owns a
3
+ //! detached OS sampler, `native-sdk.app.launches` hands the channel to
4
+ //! the single-instance listener (single_instance.zig), and
5
+ //! `native-sdk.updates` receives the updater's status records
6
+ //! (updater/engine.zig).
3
7
  const std = @import("std");
4
8
  const builtin = @import("builtin");
5
9
  const effects = @import("effects.zig");
6
10
  const clock = @import("clock.zig");
11
+ const single_instance = @import("single_instance.zig");
12
+ const update_engine = @import("../updater/engine.zig");
7
13
  const sample_interval_ms = 500;
8
14
  const max_line_bytes = 96;
9
15
 
10
16
  pub fn start(name: []const u8, payload: []const u8, handle: effects.ChannelHandle) !void {
17
+ if (std.mem.eql(u8, name, single_instance.source_name)) return single_instance.attach(payload, handle);
18
+ if (std.mem.eql(u8, name, update_engine.channel_source)) return attachUpdates(payload, handle);
11
19
  if (!std.mem.eql(u8, name, "native-sdk.process.samples")) return error.UnknownChannelSource;
12
20
  if (payload.len != 0) return error.InvalidChannelSourceArguments;
13
21
  if (comptime builtin.single_threaded) return error.UnsupportedChannelSource;
14
22
  try startSamplerThread(handle);
15
23
  }
16
24
 
25
+ /// The open `native-sdk.updates` channels, each the engine's sink. A slot
26
+ /// frees itself when its channel answers `.closed`.
27
+ const UpdateChannel = struct {
28
+ handle: effects.ChannelHandle = undefined,
29
+ used: std.atomic.Value(bool) = .init(false),
30
+
31
+ fn post(context: *anyopaque, bytes: []const u8) update_engine.Posted {
32
+ const self: *UpdateChannel = @ptrCast(@alignCast(context));
33
+ return switch (self.handle.post(bytes)) {
34
+ .accepted => .delivered,
35
+ .dropped_full, .dropped_oversized => .dropped,
36
+ .closed => {
37
+ self.used.store(false, .release);
38
+ return .closed;
39
+ },
40
+ };
41
+ }
42
+ };
43
+ var update_channels: [4]UpdateChannel = .{UpdateChannel{}} ** 4;
44
+
45
+ comptime {
46
+ // Every status record fits one post; notes travel by request.
47
+ std.debug.assert(update_engine.max_status_bytes <= effects.max_effect_channel_bytes);
48
+ }
49
+
50
+ fn attachUpdates(payload: []const u8, handle: effects.ChannelHandle) !void {
51
+ if (payload.len != 0) return error.InvalidChannelSourceArguments;
52
+ for (&update_channels) |*channel| {
53
+ if (channel.used.cmpxchgStrong(false, true, .acq_rel, .acquire) == null) {
54
+ channel.handle = handle;
55
+ update_engine.attach(.{ .context = channel, .post = UpdateChannel.post }) catch {
56
+ channel.used.store(false, .release);
57
+ return error.UnsupportedChannelSource;
58
+ };
59
+ return;
60
+ }
61
+ }
62
+ return error.UnsupportedChannelSource;
63
+ }
64
+
17
65
  fn startSamplerThread(handle: effects.ChannelHandle) std.Thread.SpawnError!void {
18
66
  const thread = try std.Thread.spawn(.{}, samplerMain, .{handle});
19
67
  thread.detach();
@@ -79,6 +79,7 @@ const relational_store = @import("relational_store.zig");
79
79
  const credentials_store = @import("credentials_store.zig");
80
80
  const file_access = @import("file_access.zig");
81
81
  const pty_transport = @import("pty.zig");
82
+ const update_engine = @import("../updater/engine.zig");
82
83
 
83
84
  /// Maximum in-flight effects (spawn slots / worker threads).
84
85
  pub const max_effects: usize = 16;
@@ -9665,7 +9666,8 @@ pub fn Effects(comptime Msg: type) type {
9665
9666
  return @import("desktop_files.zig").isName(name) or std.mem.startsWith(u8, name, "core.store.") or
9666
9667
  std.mem.eql(u8, name, "native-sdk.launch-at-login.status") or
9667
9668
  std.mem.eql(u8, name, "native-sdk.launch-at-login.set") or
9668
- std.mem.eql(u8, name, "native-sdk.time.formatLocal");
9669
+ std.mem.eql(u8, name, "native-sdk.time.formatLocal") or
9670
+ std.mem.startsWith(u8, name, update_engine.request_prefix);
9669
9671
  }
9670
9672
 
9671
9673
  fn isNativeHostSendName(name: []const u8) bool {
@@ -9826,6 +9828,15 @@ pub fn Effects(comptime Msg: type) type {
9826
9828
  self.performBoundSystemRequest(name, key, payload);
9827
9829
  return;
9828
9830
  }
9831
+ if (std.mem.startsWith(u8, name, update_engine.request_prefix)) {
9832
+ // The updater answers at once; its work runs off the loop
9833
+ // and reports through the `native-sdk.updates` channel.
9834
+ var output: [update_engine.max_response_bytes]u8 = undefined;
9835
+ const response = update_engine.handleRequest(name, payload, &output);
9836
+ self.feedHostResult(key, response.ok, response.bytes) catch {};
9837
+ if (response.quit) if (self.services) |services| services.quitApp() catch {};
9838
+ return;
9839
+ }
9829
9840
  const services = self.services orelse {
9830
9841
  self.feedHostResult(key, false, "unsupported") catch {};
9831
9842
  return;
@@ -30,6 +30,7 @@ const app_manifest = @import("app_manifest");
30
30
  const platform = @import("../platform/root.zig");
31
31
  const host_flow = @import("host_flow_policy.zig");
32
32
  const native_reference = @import("flow_reference.zig");
33
+ const update_engine = @import("../updater/engine.zig");
33
34
  const HostEventPump = @import("host_event_pump.zig").HostEventPump;
34
35
  const security = @import("../security/root.zig");
35
36
 
@@ -455,6 +456,11 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
455
456
  },
456
457
  .mark_stop => {
457
458
  self.app_stop_delivered = true;
459
+ // A release the app staged but did not restart into
460
+ // installs as the app quits, where the platform
461
+ // installs on exit (macOS); hosts may exit the process
462
+ // right after this event, so it cannot wait for run().
463
+ update_engine.finish();
458
464
  },
459
465
  .stop => {
460
466
  try app.stop(self);
@@ -12,6 +12,7 @@ const runtime_automation_snapshot = @import("automation_snapshot.zig");
12
12
  const runtime_automation_widget_dispatch = @import("automation_widget_dispatch.zig");
13
13
  const automation_commands = @import("automation_command_policy.zig");
14
14
  const launch_timing = @import("launch_timing.zig");
15
+ const update_engine = @import("../updater/engine.zig");
15
16
  const runtime_clock = @import("clock.zig");
16
17
  const runtime_state = @import("state.zig");
17
18
  const shell_layout = @import("shell_layout.zig");
@@ -524,6 +525,11 @@ pub fn RuntimeFlow(comptime Runtime: type) type {
524
525
  .app_shutdown => {
525
526
  try dispatchEvent(self, app, .{ .lifecycle = .stop });
526
527
  if (self.options.extensions) |registry| try registry.stopAll(extensionContext(self));
528
+ // A release the app staged but did not restart into
529
+ // installs as the app quits, where the platform
530
+ // installs on exit (macOS); hosts may exit the process
531
+ // right after this event, so it cannot wait for run().
532
+ update_engine.finish();
527
533
  // Marked delivered BEFORE the call: if the hook itself
528
534
  // errors, the run loop's exit path must not invoke it a
529
535
  // second time (the hook contract is exactly-once).
@@ -241,6 +241,10 @@ pub const max_session_journal_bytes = runtime_session_journal.max_session_journa
241
241
  pub const max_session_record_bytes = runtime_session_journal.max_session_record_bytes;
242
242
  pub const max_session_event_bytes = runtime_session_journal.max_session_event_bytes;
243
243
 
244
+ /// Manifest `single_instance` ownership; the launcher claims it before
245
+ /// startup and the `native-sdk.app.launches` source delivers launches.
246
+ pub const single_instance = @import("single_instance.zig");
247
+
244
248
  const runtime_clock = @import("clock.zig");
245
249
  pub const Clock = runtime_clock.Clock;
246
250
  pub const TestClock = runtime_clock.TestClock;