diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85bab999a..68eae90ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,22 @@
All notable changes to the Native SDK (formerly zero-native) will be documented in this file.
-## 0.10.0
+## 0.10.1
+### Bug Fixes
+
+- **Complete macOS distribution signing**: Developer ID packages now require an explicit secure timestamp, can notarize through a `notarytool` Keychain profile, and staple and validate both the app and final signed DMG before updater archives are created.
+
+### Contributors
+
+- @ctate
+
+
+
+## 0.10.0
+
### New Features
- **Native macOS app updates**: Apps can publish Ed25519-signed update feeds that verify downloads, atomically replace and relaunch the installed bundle, roll back safely on failure, and integrate with new manifest settings plus CLI key-generation, feed-signing, and updater-package commands (#398).
@@ -23,8 +35,6 @@ All notable changes to the Native SDK (formerly zero-native) will be documented
- @ctate
- @sepehr-safari
-
-
## 0.9.5
### New Features
diff --git a/build.zig b/build.zig
index 7f4272216..0a8c075d6 100644
--- a/build.zig
+++ b/build.zig
@@ -119,6 +119,9 @@ pub fn build(b: *std.Build) void {
_ = b.option(bool, "js-bridge", "Enable optional JavaScript bridge stubs") orelse false;
const package_target = b.option(PackageTarget, "package-target", "Package target: macos, windows, linux, ios, android") orelse .macos;
const signing_mode = b.option(SigningMode, "signing", "Signing mode: none, adhoc, identity") orelse .none;
+ const signing_identity = b.option([]const u8, "identity", "Code signing identity for distribution packages");
+ const signing_entitlements = b.option([]const u8, "entitlements", "Entitlements plist for identity signing");
+ const notary_profile = b.option([]const u8, "notary-profile", "notarytool Keychain profile for notarization");
const package_version = packageVersion(b);
const optimize_name = @tagName(optimize);
// Resolve against THIS build's root: as a dependency of a user app the
@@ -2962,6 +2965,8 @@ pub fn build(b: *std.Build) void {
});
package_run.addFileArg(embed_lib.getEmittedBin());
package_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--signing", @tagName(signing_mode), "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });
+ if (signing_identity) |identity| package_run.addArgs(&.{ "--identity", identity });
+ if (signing_entitlements) |entitlements| package_run.addArgs(&.{ "--entitlements", entitlements });
if (cef_auto_install) package_run.addArg("--cef-auto-install");
package_run.step.dependOn(&embed_lib.step);
package_run.step.dependOn(&bundle_run.step);
@@ -2999,6 +3004,13 @@ pub fn build(b: *std.Build) void {
const package_cef_smoke_step = b.step("test-package-cef-layout", "Verify macOS Chromium package layout");
package_cef_smoke_step.dependOn(&package_cef_check.step);
+ // The repository's macOS distribution helpers need a real launchable
+ // executable inside the .app. Keep the notarization helper on the same
+ // emitted host CLI binary that the signed-package smoke executes below;
+ // the SDK embed artifact is a static `.a` library and is not a valid
+ // CFBundleExecutable even though codesign can seal a bundle around it.
+ const macos_distribution_executable = host_cli_exe.getEmittedBin();
+
// Signed-package seal pin: package an ad-hoc signed bundle and prove
// the signature survives packaging intact with codesign's own strict
// verifier. This is the regression gate for the ordering bug where a
@@ -3015,7 +3027,7 @@ pub fn build(b: *std.Build) void {
const package_signing_mode: []const u8 = if (b.graph.host.result.os.tag == .macos) "adhoc" else "none";
const package_signing_run = b.addRunArtifact(host_cli_exe);
package_signing_run.addArgs(&.{ "package", "--target", "macos", "--output", "zig-out/package/native-sdk-signing-verify.app", "--binary" });
- package_signing_run.addFileArg(host_cli_exe.getEmittedBin());
+ package_signing_run.addFileArg(macos_distribution_executable);
package_signing_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--signing", package_signing_mode });
package_signing_run.has_side_effects = true;
const package_signing_check = b.addSystemCommand(&.{
@@ -3027,6 +3039,7 @@ pub fn build(b: *std.Build) void {
\\ exit 0
\\fi
\\codesign --verify --strict --deep "$app"
+ \\"$app/Contents/MacOS/native-sdk" version >/dev/null
\\grep -q "ad-hoc signed" "$app/Contents/Resources/signing-plan.txt"
\\echo "signed package verify ok"
,
@@ -3094,10 +3107,13 @@ pub fn build(b: *std.Build) void {
b.fmt("zig-out/package/native-sdk-{s}-macos-{s}.app", .{ package_version, optimize_name }),
"--binary",
});
- notarize_run.addFileArg(embed_lib.getEmittedBin());
- notarize_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--signing", "identity", "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });
+ notarize_run.addFileArg(macos_distribution_executable);
+ notarize_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--signing", "identity", "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir, "--archive", "--notarize" });
+ if (signing_identity) |identity| notarize_run.addArgs(&.{ "--identity", identity });
+ if (signing_entitlements) |entitlements| notarize_run.addArgs(&.{ "--entitlements", entitlements });
+ if (notary_profile) |profile| notarize_run.addArgs(&.{ "--notary-profile", profile });
if (cef_auto_install) notarize_run.addArg("--cef-auto-install");
- notarize_run.step.dependOn(&embed_lib.step);
+ notarize_run.step.dependOn(&host_cli_exe.step);
notarize_run.step.dependOn(&bundle_run.step);
const notarize_step = b.step("notarize", "Package, sign with identity, and notarize for macOS distribution");
notarize_step.dependOn(¬arize_run.step);
@@ -3118,6 +3134,8 @@ pub fn build(b: *std.Build) void {
});
dmg_run.addFileArg(embed_lib.getEmittedBin());
dmg_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--signing", @tagName(signing_mode), "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir, "--archive" });
+ if (signing_identity) |identity| dmg_run.addArgs(&.{ "--identity", identity });
+ if (signing_entitlements) |entitlements| dmg_run.addArgs(&.{ "--entitlements", entitlements });
if (cef_auto_install) dmg_run.addArg("--cef-auto-install");
dmg_run.step.dependOn(&embed_lib.step);
dmg_run.step.dependOn(&bundle_run.step);
diff --git a/docs/src/app/docs/cli/page.mdx b/docs/src/app/docs/cli/page.mdx
index 96d752f87..20f57eb67 100644
--- a/docs/src/app/docs/cli/page.mdx
+++ b/docs/src/app/docs/cli/page.mdx
@@ -148,8 +148,10 @@ Package the app for distribution. The manifest is picked up at `app.json` (falli
Code signing identity name.
--entitlements
Path to entitlements file.
- --team-id
- Apple Developer Team ID.
+ --notarize
+ Submit the final macOS artifact to Apple's notary service, then staple and validate the accepted ticket. Requires identity signing.
+ --notary-profile
+ Name of a notarytool Keychain profile created with xcrun notarytool store-credentials.
--archive
Create a distributable archive. On macOS this is a styled DMG with the app, an Applications alias, a generated or custom background, and the Finder layout declared by the app manifest.
diff --git a/docs/src/app/docs/packaging/signing/page.mdx b/docs/src/app/docs/packaging/signing/page.mdx
index 56a1e427f..cdf484251 100644
--- a/docs/src/app/docs/packaging/signing/page.mdx
+++ b/docs/src/app/docs/packaging/signing/page.mdx
@@ -10,6 +10,8 @@ Sign the bundle with a Developer ID:
native package --target macos --signing identity --identity "Developer ID Application: Your Name"
```
+Identity signing enables the hardened runtime and explicitly requests Apple's secure timestamp. If the timestamp service cannot be reached, packaging fails instead of producing a distribution signature that notarization will reject.
+
Signing modes:
@@ -69,25 +71,55 @@ For distribution without any dialog, sign with a Developer ID (`--signing identi
Path to entitlements file (e.g. assets/native-sdk.entitlements) |
- --team-id |
- Apple Developer Team ID |
+ --notarize |
+ Submit the final macOS artifact, then staple and validate the accepted ticket |
+
+
+ --notary-profile |
+ Name of a notarytool Keychain profile created with store-credentials |
## Notarization
-The framework repository includes a `zig build notarize` helper for local release testing:
+Store notarization credentials in Keychain once. A team App Store Connect API key is the normal CI choice:
+
+```bash
+xcrun notarytool store-credentials "my-app-release" \
+ --key /path/to/AuthKey_KEYID.p8 \
+ --key-id KEYID \
+ --issuer ISSUER_UUID
+```
+
+Apple ID credentials work too:
+
+```bash
+xcrun notarytool store-credentials "my-app-release" \
+ --apple-id "you@example.com" \
+ --team-id TEAMID \
+ --password APP_SPECIFIC_PASSWORD
+```
+
+Then package, timestamp-sign, notarize, staple, and validate in one command:
```bash
-zig build notarize
+native package --target macos \
+ --signing identity \
+ --identity "Developer ID Application: Your Name (TEAMID)" \
+ --archive \
+ --notarize \
+ --notary-profile "my-app-release"
```
-Generated apps should use `native package --target macos --signing identity ... --archive` unless they add their own `notarize` build step. This helper does not invoke `xcrun notarytool` directly. After the signed DMG is created, submit and staple it manually (use the archive path printed by the package command):
+With `--archive`, Native SDK first submits the signed app, staples and validates it, then builds and signs the DMG and submits, staples, and validates that final image. Without `--archive`, only the app submission runs. Any updater ZIP is created afterward from the stapled app.
+
+The framework repository exposes the same path for local release testing:
```bash
-xcrun notarytool submit zig-out/package/your-app-1.0.0-macos-ReleaseFast.dmg --apple-id "you@example.com" --team-id "TEAMID" --password "@keychain:AC_PASSWORD" --wait
-xcrun stapler staple zig-out/package/your-app-1.0.0-macos-ReleaseFast.dmg
+zig build notarize \
+ -Didentity="Developer ID Application: Your Name (TEAMID)" \
+ -Dnotary-profile="my-app-release"
```
## Chromium apps
diff --git a/examples/gpu-components/package.json b/examples/gpu-components/package.json
index e8454badd..e9602c217 100644
--- a/examples/gpu-components/package.json
+++ b/examples/gpu-components/package.json
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor surface for the TypeScript core; the native CLI builds without node_modules.",
"dependencies": {
- "@native-sdk/core": "0.10.0"
+ "@native-sdk/core": "0.10.1"
}
}
diff --git a/examples/kanban/package.json b/examples/kanban/package.json
index 551c6654a..a0ed73bc0 100644
--- a/examples/kanban/package.json
+++ b/examples/kanban/package.json
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor surface for the TypeScript core; the native CLI builds without node_modules.",
"dependencies": {
- "@native-sdk/core": "0.10.0"
+ "@native-sdk/core": "0.10.1"
}
}
diff --git a/examples/menu-bar/package.json b/examples/menu-bar/package.json
index 7ad201dd4..83ba66958 100644
--- a/examples/menu-bar/package.json
+++ b/examples/menu-bar/package.json
@@ -3,6 +3,6 @@
"private": true,
"description": "TypeScript + Native markup menu-bar lifecycle example.",
"dependencies": {
- "@native-sdk/core": "0.10.0"
+ "@native-sdk/core": "0.10.1"
}
}
diff --git a/examples/record-store/package.json b/examples/record-store/package.json
index 2264f502a..216a85340 100644
--- a/examples/record-store/package.json
+++ b/examples/record-store/package.json
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor surface for the TypeScript core; the native CLI builds without node_modules.",
"dependencies": {
- "@native-sdk/core": "0.10.0"
+ "@native-sdk/core": "0.10.1"
}
}
diff --git a/examples/relational-notes/package.json b/examples/relational-notes/package.json
index 60f2d6d9d..597fd8ce3 100644
--- a/examples/relational-notes/package.json
+++ b/examples/relational-notes/package.json
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor and versioning surface only: stock TypeScript tooling resolves @native-sdk/core from here. The native CLI never reads it and builds with node_modules absent.",
"dependencies": {
- "@native-sdk/core": "0.10.0"
+ "@native-sdk/core": "0.10.1"
}
}
diff --git a/examples/soundboard-ts/package.json b/examples/soundboard-ts/package.json
index 92a3267a2..af88b0069 100644
--- a/examples/soundboard-ts/package.json
+++ b/examples/soundboard-ts/package.json
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor and versioning surface only: stock TypeScript tooling resolves @native-sdk/core from here. The native CLI never reads it and builds with node_modules absent.",
"dependencies": {
- "@native-sdk/core": "0.10.0"
+ "@native-sdk/core": "0.10.1"
}
}
diff --git a/examples/system-monitor-ts/package.json b/examples/system-monitor-ts/package.json
index c2f1a2181..fd9801412 100644
--- a/examples/system-monitor-ts/package.json
+++ b/examples/system-monitor-ts/package.json
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor and versioning surface only: stock TypeScript tooling resolves @native-sdk/core from here. The native CLI never reads it and builds with node_modules absent.",
"dependencies": {
- "@native-sdk/core": "0.10.0"
+ "@native-sdk/core": "0.10.1"
}
}
diff --git a/examples/voice-memo/package.json b/examples/voice-memo/package.json
index f85685321..194a4f3c6 100644
--- a/examples/voice-memo/package.json
+++ b/examples/voice-memo/package.json
@@ -3,6 +3,6 @@
"private": true,
"description": "Editor and versioning surface only: stock TypeScript tooling resolves @native-sdk/core from here. The native CLI never reads it and builds with node_modules absent.",
"dependencies": {
- "@native-sdk/core": "0.10.0"
+ "@native-sdk/core": "0.10.1"
}
}
diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json
index 6fdda3886..d41e8ea4d 100644
--- a/packages/core/package-lock.json
+++ b/packages/core/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@native-sdk/core",
- "version": "0.10.0",
+ "version": "0.10.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@native-sdk/core",
- "version": "0.10.0",
+ "version": "0.10.1",
"dependencies": {
"scriptc": "0.0.35"
},
diff --git a/packages/core/package.json b/packages/core/package.json
index 8dc144486..01ea5b2b7 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/core",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The TypeScript authoring tier: the app-core subset, its checker and contract frontend, the exact-pinned core compiler dependency, and the SDK module cores import",
"repository": {
"type": "git",
diff --git a/packages/native-sdk/npm/darwin-arm64/package.json b/packages/native-sdk/npm/darwin-arm64/package.json
index 84d3b9428..030ca9349 100644
--- a/packages/native-sdk/npm/darwin-arm64/package.json
+++ b/packages/native-sdk/npm/darwin-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli-darwin-arm64",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The native CLI binary for macOS on Apple silicon (arm64)",
"os": [
"darwin"
diff --git a/packages/native-sdk/npm/darwin-x64/package.json b/packages/native-sdk/npm/darwin-x64/package.json
index c12f898a7..9fcb9c25e 100644
--- a/packages/native-sdk/npm/darwin-x64/package.json
+++ b/packages/native-sdk/npm/darwin-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli-darwin-x64",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The native CLI binary for macOS on Intel (x64)",
"os": [
"darwin"
diff --git a/packages/native-sdk/npm/linux-arm64-gnu/package.json b/packages/native-sdk/npm/linux-arm64-gnu/package.json
index e43b6e888..9b85f82ec 100644
--- a/packages/native-sdk/npm/linux-arm64-gnu/package.json
+++ b/packages/native-sdk/npm/linux-arm64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli-linux-arm64-gnu",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The native CLI binary for Linux arm64 (glibc)",
"os": [
"linux"
diff --git a/packages/native-sdk/npm/linux-arm64-musl/package.json b/packages/native-sdk/npm/linux-arm64-musl/package.json
index b85727251..5fc19febf 100644
--- a/packages/native-sdk/npm/linux-arm64-musl/package.json
+++ b/packages/native-sdk/npm/linux-arm64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli-linux-arm64-musl",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The native CLI binary for Linux arm64 (musl)",
"os": [
"linux"
diff --git a/packages/native-sdk/npm/linux-x64-gnu/package.json b/packages/native-sdk/npm/linux-x64-gnu/package.json
index f7ed863db..cbc8ba26f 100644
--- a/packages/native-sdk/npm/linux-x64-gnu/package.json
+++ b/packages/native-sdk/npm/linux-x64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli-linux-x64-gnu",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The native CLI binary for Linux x64 (glibc)",
"os": [
"linux"
diff --git a/packages/native-sdk/npm/linux-x64-musl/package.json b/packages/native-sdk/npm/linux-x64-musl/package.json
index 7e65be07a..2bd66103a 100644
--- a/packages/native-sdk/npm/linux-x64-musl/package.json
+++ b/packages/native-sdk/npm/linux-x64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli-linux-x64-musl",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The native CLI binary for Linux x64 (musl)",
"os": [
"linux"
diff --git a/packages/native-sdk/npm/win32-arm64/package.json b/packages/native-sdk/npm/win32-arm64/package.json
index 499f49c38..bf17c369b 100644
--- a/packages/native-sdk/npm/win32-arm64/package.json
+++ b/packages/native-sdk/npm/win32-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli-win32-arm64",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The native CLI binary for Windows on ARM (arm64)",
"os": [
"win32"
diff --git a/packages/native-sdk/npm/win32-x64/package.json b/packages/native-sdk/npm/win32-x64/package.json
index 85d8e9807..c1ad19df2 100644
--- a/packages/native-sdk/npm/win32-x64/package.json
+++ b/packages/native-sdk/npm/win32-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli-win32-x64",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The native CLI binary for Windows x64",
"os": [
"win32"
diff --git a/packages/native-sdk/package.json b/packages/native-sdk/package.json
index 2820710bd..26aadd40f 100644
--- a/packages/native-sdk/package.json
+++ b/packages/native-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@native-sdk/cli",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "The Native SDK: the complete toolkit for building native desktop applications — declarative markup, native rendering, WebView surfaces, and OS capabilities",
"type": "module",
"engines": {
@@ -39,14 +39,14 @@
"scriptc": "0.0.35"
},
"optionalDependencies": {
- "@native-sdk/cli-darwin-arm64": "0.10.0",
- "@native-sdk/cli-darwin-x64": "0.10.0",
- "@native-sdk/cli-linux-arm64-gnu": "0.10.0",
- "@native-sdk/cli-linux-arm64-musl": "0.10.0",
- "@native-sdk/cli-linux-x64-gnu": "0.10.0",
- "@native-sdk/cli-linux-x64-musl": "0.10.0",
- "@native-sdk/cli-win32-arm64": "0.10.0",
- "@native-sdk/cli-win32-x64": "0.10.0"
+ "@native-sdk/cli-darwin-arm64": "0.10.1",
+ "@native-sdk/cli-darwin-x64": "0.10.1",
+ "@native-sdk/cli-linux-arm64-gnu": "0.10.1",
+ "@native-sdk/cli-linux-arm64-musl": "0.10.1",
+ "@native-sdk/cli-linux-x64-gnu": "0.10.1",
+ "@native-sdk/cli-linux-x64-musl": "0.10.1",
+ "@native-sdk/cli-win32-arm64": "0.10.1",
+ "@native-sdk/cli-win32-x64": "0.10.1"
},
"repository": {
"type": "git",
diff --git a/skill-data/core/references/web-engines-packaging-debugging.md b/skill-data/core/references/web-engines-packaging-debugging.md
index b941f7cd2..1d60b8e17 100644
--- a/skill-data/core/references/web-engines-packaging-debugging.md
+++ b/skill-data/core/references/web-engines-packaging-debugging.md
@@ -106,9 +106,10 @@ Signing modes:
native package --target macos --signing none
native package --target macos --signing adhoc
native package --target macos --signing identity --identity "Developer ID Application: Your Name"
+native package --target macos --signing identity --identity "Developer ID Application: Your Name" --archive --notarize --notary-profile "my-app-release"
```
-For Chromium apps, verify the CEF framework and resources are included and signed before notarization.
+Identity signing enables the hardened runtime and requires a secure timestamp. `--notarize` uses a `notarytool` Keychain profile, submits the final DMG when `--archive` is present, and staples and validates the accepted tickets. For Chromium apps, verify the CEF framework and resources are included and signed before notarization.
## Linux and Windows packages
diff --git a/src/tooling/codesign.zig b/src/tooling/codesign.zig
index d77f04ea0..ca88095c5 100644
--- a/src/tooling/codesign.zig
+++ b/src/tooling/codesign.zig
@@ -13,14 +13,13 @@ pub const CodesignArgs = struct {
identity: []const u8 = "-",
entitlements: ?[]const u8 = null,
hardened_runtime: bool = false,
+ secure_timestamp: bool = false,
deep: bool = true,
};
pub const NotarizeArgs = struct {
- app_path: []const u8,
- team_id: []const u8,
- apple_id: ?[]const u8 = null,
- password_keychain_item: ?[]const u8 = null,
+ artifact_path: []const u8,
+ keychain_profile: []const u8,
};
// Every command in this pipeline is built as an ARGV ARRAY, never as a
@@ -29,7 +28,7 @@ pub const NotarizeArgs = struct {
// replaced split `My App.app` into two arguments, codesign signed
// nothing, and the failure never surfaced.
-pub const sign_argv_capacity = 10;
+pub const sign_argv_capacity = 11;
pub fn signArgv(buffer: *[sign_argv_capacity][]const u8, args: CodesignArgs) []const []const u8 {
var len: usize = 0;
@@ -51,6 +50,10 @@ pub fn signArgv(buffer: *[sign_argv_capacity][]const u8, args: CodesignArgs) []c
buffer[len] = "runtime";
len += 1;
}
+ if (args.secure_timestamp) {
+ buffer[len] = "--timestamp";
+ len += 1;
+ }
if (args.entitlements) |ent| {
buffer[len] = "--entitlements";
len += 1;
@@ -67,6 +70,11 @@ pub fn verifyArgv(buffer: *[6][]const u8, app_path: []const u8) []const []const
return buffer;
}
+pub fn verifyArtifactArgv(buffer: *[5][]const u8, artifact_path: []const u8) []const []const u8 {
+ buffer.* = .{ "codesign", "--verify", "--strict", "--verbose=2", artifact_path };
+ return buffer;
+}
+
pub fn zipArgv(buffer: *[6][]const u8, app_path: []const u8, zip_path: []const u8) []const []const u8 {
buffer.* = .{ "ditto", "-c", "-k", "--keepParent", app_path, zip_path };
return buffer;
@@ -77,37 +85,18 @@ pub fn stapleArgv(buffer: *[4][]const u8, app_path: []const u8) []const []const
return buffer;
}
-pub const notarize_submit_argv_capacity = 11;
+pub fn stapleValidateArgv(buffer: *[4][]const u8, app_path: []const u8) []const []const u8 {
+ buffer.* = .{ "xcrun", "stapler", "validate", app_path };
+ return buffer;
+}
-pub fn notarizeSubmitArgv(buffer: *[notarize_submit_argv_capacity][]const u8, zip_path: []const u8, password_value: ?[]const u8, args: NotarizeArgs) []const []const u8 {
- var len: usize = 0;
- buffer[len] = "xcrun";
- len += 1;
- buffer[len] = "notarytool";
- len += 1;
- buffer[len] = "submit";
- len += 1;
- buffer[len] = zip_path;
- len += 1;
- buffer[len] = "--team-id";
- len += 1;
- buffer[len] = args.team_id;
- len += 1;
- if (args.apple_id) |apple_id| {
- buffer[len] = "--apple-id";
- len += 1;
- buffer[len] = apple_id;
- len += 1;
- }
- if (password_value) |value| {
- buffer[len] = "--password";
- len += 1;
- buffer[len] = value;
- len += 1;
- }
- buffer[len] = "--wait";
- len += 1;
- return buffer[0..len];
+pub fn notarizeSubmitArgv(buffer: *[9][]const u8, artifact_path: []const u8, keychain_profile: []const u8) []const []const u8 {
+ buffer.* = .{
+ "xcrun", "notarytool", "submit",
+ artifact_path, "--keychain-profile", keychain_profile,
+ "--wait", "--output-format", "json",
+ };
+ return buffer;
}
pub fn signAdHoc(allocator: std.mem.Allocator, io: std.Io, app_path: []const u8) !SignResult {
@@ -120,10 +109,20 @@ pub fn signIdentity(allocator: std.mem.Allocator, io: std.Io, app_path: []const
.identity = identity,
.entitlements = entitlements,
.hardened_runtime = true,
+ .secure_timestamp = true,
.deep = true,
});
}
+pub fn signIdentityArtifact(allocator: std.mem.Allocator, io: std.Io, artifact_path: []const u8, identity: []const u8) !SignResult {
+ return runSign(allocator, io, .{
+ .app_path = artifact_path,
+ .identity = identity,
+ .secure_timestamp = true,
+ .deep = false,
+ });
+}
+
/// `codesign --verify --deep --strict` over the signed bundle: the same
/// check Gatekeeper and an Apple silicon launch effectively run, so a
/// signature that only LOOKS applied (stale seal, unsigned nested code)
@@ -133,37 +132,58 @@ pub fn verify(allocator: std.mem.Allocator, io: std.Io, app_path: []const u8) !S
return runTool(allocator, io, verifyArgv(&buffer, app_path));
}
-pub fn notarize(allocator: std.mem.Allocator, io: std.Io, args: NotarizeArgs) !SignResult {
- const zip_path = try std.fmt.allocPrint(allocator, "{s}.zip", .{args.app_path});
+pub fn verifyArtifact(allocator: std.mem.Allocator, io: std.Io, artifact_path: []const u8) !SignResult {
+ var buffer: [5][]const u8 = undefined;
+ return runTool(allocator, io, verifyArtifactArgv(&buffer, artifact_path));
+}
+
+pub fn notarizeApp(allocator: std.mem.Allocator, io: std.Io, args: NotarizeArgs) !SignResult {
+ const zip_path = try std.fmt.allocPrint(allocator, "{s}.notarize.zip", .{args.artifact_path});
defer allocator.free(zip_path);
+ defer std.Io.Dir.cwd().deleteFile(io, zip_path) catch {};
- {
- var zip_buffer: [6][]const u8 = undefined;
- const zip_result = try runTool(allocator, io, zipArgv(&zip_buffer, args.app_path, zip_path));
- if (!zip_result.ok) return failedStep(allocator, "ditto (zip for notarization)", zip_result);
- allocator.free(zip_result.message);
- }
+ var zip_buffer: [6][]const u8 = undefined;
+ const zip_result = try runTool(allocator, io, zipArgv(&zip_buffer, args.artifact_path, zip_path));
+ if (!zip_result.ok) return failedStep(allocator, "ditto (zip for notarization)", zip_result);
+ allocator.free(zip_result.message);
- {
- const password_value: ?[]const u8 = if (args.password_keychain_item) |item|
- try std.fmt.allocPrint(allocator, "@keychain:{s}", .{item})
- else
- null;
- defer if (password_value) |value| allocator.free(value);
- var submit_buffer: [notarize_submit_argv_capacity][]const u8 = undefined;
- const submit_result = try runTool(allocator, io, notarizeSubmitArgv(&submit_buffer, zip_path, password_value, args));
- if (!submit_result.ok) return failedStep(allocator, "notarytool submit", submit_result);
- allocator.free(submit_result.message);
- }
+ return notarizeArtifact(allocator, io, .{
+ .artifact_path = args.artifact_path,
+ .keychain_profile = args.keychain_profile,
+ }, zip_path);
+}
- {
- var staple_buffer: [4][]const u8 = undefined;
- const staple_result = try runTool(allocator, io, stapleArgv(&staple_buffer, args.app_path));
- if (!staple_result.ok) return failedStep(allocator, "stapler staple", staple_result);
- allocator.free(staple_result.message);
- }
+pub fn notarizeArtifact(allocator: std.mem.Allocator, io: std.Io, args: NotarizeArgs, submission_path: ?[]const u8) !SignResult {
+ var submit_buffer: [9][]const u8 = undefined;
+ const submit_result = try runTool(allocator, io, notarizeSubmitArgv(&submit_buffer, submission_path orelse args.artifact_path, args.keychain_profile));
+ if (!submit_result.ok) return failedStep(allocator, "notarytool submit", submit_result);
+ if (!notarizationAccepted(allocator, submit_result.message)) return failedStep(allocator, "notarytool returned a non-accepted status", submit_result);
+ allocator.free(submit_result.message);
- return .{ .ok = true, .message = try allocator.dupe(u8, "notarization complete") };
+ var staple_buffer: [4][]const u8 = undefined;
+ const staple_result = try runTool(allocator, io, stapleArgv(&staple_buffer, args.artifact_path));
+ if (!staple_result.ok) return failedStep(allocator, "stapler staple", staple_result);
+ allocator.free(staple_result.message);
+
+ var validate_buffer: [4][]const u8 = undefined;
+ const validate_result = try runTool(allocator, io, stapleValidateArgv(&validate_buffer, args.artifact_path));
+ if (!validate_result.ok) return failedStep(allocator, "stapler validate", validate_result);
+ allocator.free(validate_result.message);
+
+ return .{ .ok = true, .message = try allocator.dupe(u8, "notarization accepted, stapled, and validated") };
+}
+
+fn notarizationAccepted(allocator: std.mem.Allocator, output: []const u8) bool {
+ const start = std.mem.indexOfScalar(u8, output, '{') orelse return false;
+ const end = std.mem.lastIndexOfScalar(u8, output, '}') orelse return false;
+ if (end < start) return false;
+ var parsed = std.json.parseFromSlice(std.json.Value, allocator, output[start .. end + 1], .{}) catch return false;
+ defer parsed.deinit();
+ const status = switch (parsed.value) {
+ .object => |object| object.get("status") orelse return false,
+ else => return false,
+ };
+ return status == .string and std.mem.eql(u8, status.string, "Accepted");
}
fn failedStep(allocator: std.mem.Allocator, step: []const u8, result: SignResult) !SignResult {
@@ -215,13 +235,30 @@ test "identity sign argv includes runtime and entitlements as single arguments"
.identity = "Developer ID Application: Test Person (ABCD1234)",
.entitlements = "assets dir/native-sdk.entitlements",
.hardened_runtime = true,
+ .secure_timestamp = true,
+ });
+ const expected = [_][]const u8{
+ "codesign", "--sign",
+ "Developer ID Application: Test Person (ABCD1234)", "--force",
+ "--deep", "--options",
+ "runtime", "--timestamp",
+ "--entitlements", "assets dir/native-sdk.entitlements",
+ "/tmp/My Demo App.app",
+ };
+ try expectArgv(&expected, argv);
+}
+
+test "distribution artifact sign argv requests a secure timestamp without app-only options" {
+ var buffer: [sign_argv_capacity][]const u8 = undefined;
+ const argv = signArgv(&buffer, .{
+ .app_path = "/tmp/My Demo App.dmg",
+ .identity = "Developer ID Application: Test Person (ABCD1234)",
+ .secure_timestamp = true,
+ .deep = false,
});
const expected = [_][]const u8{
- "codesign", "--sign",
- "Developer ID Application: Test Person (ABCD1234)", "--force",
- "--deep", "--options",
- "runtime", "--entitlements",
- "assets dir/native-sdk.entitlements", "/tmp/My Demo App.app",
+ "codesign", "--sign", "Developer ID Application: Test Person (ABCD1234)",
+ "--force", "--timestamp", "/tmp/My Demo App.dmg",
};
try expectArgv(&expected, argv);
}
@@ -233,14 +270,17 @@ test "verify argv runs the strict deep check on the bundle path" {
try expectArgv(&expected, argv);
}
-test "notarize submit argv includes team id, keychain password, and wait" {
- var buffer: [notarize_submit_argv_capacity][]const u8 = undefined;
- const argv = notarizeSubmitArgv(&buffer, "/tmp/My Demo App.app.zip", "@keychain:AC_PASSWORD", .{
- .app_path = "/tmp/My Demo App.app",
- .team_id = "ABCD1234",
- .apple_id = "dev@example.com",
- });
- const expected = [_][]const u8{ "xcrun", "notarytool", "submit", "/tmp/My Demo App.app.zip", "--team-id", "ABCD1234", "--apple-id", "dev@example.com", "--password", "@keychain:AC_PASSWORD", "--wait" };
+test "artifact verify argv runs the strict check without bundle recursion" {
+ var buffer: [5][]const u8 = undefined;
+ const argv = verifyArtifactArgv(&buffer, "/tmp/My Demo App.dmg");
+ const expected = [_][]const u8{ "codesign", "--verify", "--strict", "--verbose=2", "/tmp/My Demo App.dmg" };
+ try expectArgv(&expected, argv);
+}
+
+test "notarize submit argv uses a keychain profile, waits, and requests JSON" {
+ var buffer: [9][]const u8 = undefined;
+ const argv = notarizeSubmitArgv(&buffer, "/tmp/My Demo App.dmg", "vercel-release");
+ const expected = [_][]const u8{ "xcrun", "notarytool", "submit", "/tmp/My Demo App.dmg", "--keychain-profile", "vercel-release", "--wait", "--output-format", "json" };
try expectArgv(&expected, argv);
}
@@ -250,12 +290,23 @@ test "staple and zip argv carry spaced paths as single arguments" {
const staple_expected = [_][]const u8{ "xcrun", "stapler", "staple", "/tmp/My Demo App.app" };
try expectArgv(&staple_expected, staple);
+ var validate_buffer: [4][]const u8 = undefined;
+ const validate = stapleValidateArgv(&validate_buffer, "/tmp/My Demo App.app");
+ const validate_expected = [_][]const u8{ "xcrun", "stapler", "validate", "/tmp/My Demo App.app" };
+ try expectArgv(&validate_expected, validate);
+
var zip_buffer: [6][]const u8 = undefined;
const zip = zipArgv(&zip_buffer, "/tmp/My Demo App.app", "/tmp/My Demo App.app.zip");
const zip_expected = [_][]const u8{ "ditto", "-c", "-k", "--keepParent", "/tmp/My Demo App.app", "/tmp/My Demo App.app.zip" };
try expectArgv(&zip_expected, zip);
}
+test "notarization accepts only an explicit Accepted JSON status" {
+ try std.testing.expect(notarizationAccepted(std.testing.allocator, "{\"id\":\"123\",\"status\":\"Accepted\"}\n"));
+ try std.testing.expect(!notarizationAccepted(std.testing.allocator, "{\"id\":\"123\",\"status\":\"Invalid\"}\n"));
+ try std.testing.expect(!notarizationAccepted(std.testing.allocator, "not JSON"));
+}
+
test "a failing tool surfaces its own output, not a silent success" {
if (@import("builtin").os.tag == .windows) return error.SkipZigTest;
// `false` exits 1 with no output: the result must be a failure.
diff --git a/src/tooling/package.zig b/src/tooling/package.zig
index 937d9b154..1f12e6c12 100644
--- a/src/tooling/package.zig
+++ b/src/tooling/package.zig
@@ -54,7 +54,6 @@ pub const SigningConfig = struct {
identity: ?[]const u8 = null,
entitlements: ?[]const u8 = null,
profile: ?[]const u8 = null,
- team_id: ?[]const u8 = null,
};
pub const PackageOptions = struct {
@@ -83,6 +82,10 @@ pub const PackageOptions = struct {
cef_dir: []const u8 = web_engine_tool.default_cef_dir,
signing: SigningConfig = .{},
archive: bool = false,
+ /// Submit the final macOS distribution artifact to Apple's notary service,
+ /// then staple and validate its ticket. Requires identity signing and a
+ /// notarytool Keychain profile in `signing.profile`.
+ notarize: bool = false,
/// Emit the ZIP consumed by the native updater. macOS only; unlike the
/// user-facing DMG, this archive contains exactly the packaged .app.
update_archive: bool = false,
@@ -152,6 +155,7 @@ pub const PackageStats = struct {
/// the proof behind the report's "signed, verified" line. A package
/// whose signing or verification fails never produces stats at all.
signing_verified: bool = false,
+ notarized: bool = false,
asset_count: usize = 0,
web_engine: WebEngine = .system,
web_layer: ?manifest_tool.WebLayer = null,
@@ -221,6 +225,20 @@ pub fn createPackage(allocator: std.mem.Allocator, io: std.Io, options: PackageO
return err;
};
try validateWebEngineTarget(options.target, options.web_engine);
+ if (options.notarize) {
+ if (options.target != .macos) {
+ std.debug.print("error: notarization is supported only for macOS packages\n", .{});
+ return error.UnsupportedNotarizationTarget;
+ }
+ if (options.signing.mode != .identity) {
+ std.debug.print("error: --notarize requires --signing identity and a Developer ID Application certificate\n", .{});
+ return error.NotarizationRequiresIdentity;
+ }
+ if (options.signing.profile == null) {
+ std.debug.print("error: --notarize requires --notary-profile ; create it with `xcrun notarytool store-credentials `\n", .{});
+ return error.MissingNotaryProfile;
+ }
+ }
if (options.metadata.updates.enabled() and options.target == .macos and options.web_engine == .chromium) {
std.debug.print("error: native updates currently require the system macOS host; package with --web-engine system or remove the updates block\n", .{});
return error.UnsupportedUpdateHost;
@@ -241,12 +259,24 @@ pub fn createPackage(allocator: std.mem.Allocator, io: std.Io, options: PackageO
.ios => try createIosArtifact(allocator, io, options),
.android => try createAndroidArtifact(allocator, io, options),
};
+ if (options.notarize) {
+ try runNotarization(allocator, io, options.output_path, options.signing.profile.?, true);
+ }
if (options.archive) {
const archive_path = try createArchive(allocator, io, options);
if (archive_path) |path| {
stats.archive_path = path;
+ if (options.target == .macos and options.signing.mode == .identity) {
+ try signDistributionArtifact(allocator, io, path, options.signing.identity.?);
+ }
+ if (options.notarize) {
+ try runNotarization(allocator, io, path, options.signing.profile.?, false);
+ }
}
}
+ if (options.notarize) {
+ stats.notarized = true;
+ }
if (options.update_archive) {
if (options.target != .macos) return error.UnsupportedUpdateTarget;
if (!options.metadata.updates.enabled()) return error.UpdatesNotConfigured;
@@ -286,6 +316,9 @@ pub fn printDiagnostic(stats: PackageStats) void {
if (stats.signing_verified) {
std.debug.print(" signing: {s} (signed, verified)\n", .{@tagName(stats.signing_mode)});
}
+ if (stats.notarized) {
+ std.debug.print(" notarization: accepted, stapled, validated\n", .{});
+ }
if (stats.windows_subsystem) |subsystem| {
switch (subsystem) {
.console => std.debug.print(" subsystem: console (a terminal window opens behind the app - rebuild with `native build`)\n", .{}),
@@ -1721,6 +1754,33 @@ fn runSigning(allocator: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, options
return true;
}
+fn signDistributionArtifact(allocator: std.mem.Allocator, io: std.Io, path: []const u8, identity: []const u8) !void {
+ const signed = try codesign.signIdentityArtifact(allocator, io, path, identity);
+ defer allocator.free(signed.message);
+ if (!signed.ok) {
+ std.debug.print("error: code signing distribution artifact {s} with \"{s}\" failed:\n{s}\n", .{ path, identity, trimmedToolOutput(signed.message) });
+ return error.SigningFailed;
+ }
+ const verified = try codesign.verifyArtifact(allocator, io, path);
+ defer allocator.free(verified.message);
+ if (!verified.ok) {
+ std.debug.print("error: signed distribution artifact {s} failed strict codesign verification:\n{s}\n", .{ path, trimmedToolOutput(verified.message) });
+ return error.SignatureVerificationFailed;
+ }
+}
+
+fn runNotarization(allocator: std.mem.Allocator, io: std.Io, path: []const u8, profile: []const u8, app_bundle: bool) !void {
+ const result = if (app_bundle)
+ try codesign.notarizeApp(allocator, io, .{ .artifact_path = path, .keychain_profile = profile })
+ else
+ try codesign.notarizeArtifact(allocator, io, .{ .artifact_path = path, .keychain_profile = profile }, null);
+ defer allocator.free(result.message);
+ if (!result.ok) {
+ std.debug.print("error: notarization failed for {s}:\n{s}\n", .{ path, trimmedToolOutput(result.message) });
+ return error.NotarizationFailed;
+ }
+}
+
/// codesign's output, trimmed of trailing newlines so the teaching
/// message's fix line lands directly under it (the output itself stays
/// verbatim).
@@ -3630,6 +3690,30 @@ test "identity signing without an identity is a loud failure, not a silent unsig
}));
}
+test "notarization refuses unsigned non-macos and uncredentialed packages before artifact creation" {
+ const metadata: manifest_tool.Metadata = .{ .id = "dev.example.notarize", .name = "notarize-demo", .version = "1.0.0" };
+ try std.testing.expectError(error.NotarizationRequiresIdentity, createPackage(std.testing.allocator, std.testing.io, .{
+ .metadata = metadata,
+ .target = .macos,
+ .output_path = ".zig-cache/test-notarize-unsigned.app",
+ .notarize = true,
+ }));
+ try std.testing.expectError(error.UnsupportedNotarizationTarget, createPackage(std.testing.allocator, std.testing.io, .{
+ .metadata = metadata,
+ .target = .linux,
+ .output_path = ".zig-cache/test-notarize-linux",
+ .signing = .{ .mode = .identity, .identity = "Developer ID Application: Test" },
+ .notarize = true,
+ }));
+ try std.testing.expectError(error.MissingNotaryProfile, createPackage(std.testing.allocator, std.testing.io, .{
+ .metadata = metadata,
+ .target = .macos,
+ .output_path = ".zig-cache/test-notarize-no-profile.app",
+ .signing = .{ .mode = .identity, .identity = "Developer ID Application: Test" },
+ .notarize = true,
+ }));
+}
+
test "native-only windows package ships no WebView2 loader and reports web layer none" {
var cwd = std.Io.Dir.cwd();
const root = ".zig-cache/test-package-native-only-windows";
diff --git a/tools/native-sdk/main.zig b/tools/native-sdk/main.zig
index 33fd05d31..78bcd788e 100644
--- a/tools/native-sdk/main.zig
+++ b/tools/native-sdk/main.zig
@@ -6,7 +6,7 @@ const tooling = @import("tooling");
const automation_protocol = @import("automation_protocol");
const cli_build_info = @import("cli_build_info");
-const version = "0.10.0";
+const version = "0.10.1";
pub fn main(init: std.process.Init) !void {
const allocator = init.arena.allocator();
@@ -204,9 +204,9 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("bundled {d} assets into {s}\n", .{ stats.asset_count, output_dir });
} else if (std.mem.eql(u8, command, "package")) {
checkVerbFlags("package", args[2..], .{
- .usage = "package [--target macos] [--output path] [--binary path] [--service-binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--team-id id] [--archive] [--update-archive]",
- .value_flags = &.{ "--manifest", "--target", "--output", "--binary", "--service-binary", "--assets", "--web-engine", "--web-layer", "--cef-dir", "--signing", "--identity", "--entitlements", "--team-id", "--optimize" },
- .bool_flags = &.{ "--cef-auto-install", "--archive", "--update-archive" },
+ .usage = "package [--target macos] [--output path] [--binary path] [--service-binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--notarize --notary-profile name] [--archive] [--update-archive]",
+ .value_flags = &.{ "--manifest", "--target", "--output", "--binary", "--service-binary", "--assets", "--web-engine", "--web-layer", "--cef-dir", "--signing", "--identity", "--entitlements", "--notary-profile", "--optimize" },
+ .bool_flags = &.{ "--cef-auto-install", "--notarize", "--archive", "--update-archive" },
});
const manifest_path = try flagValue(args, "--manifest") orelse tooling.manifest.defaultPath(init.io) orelse "app.json";
const metadata = tooling.manifest.readMetadata(allocator, init.io, manifest_path) catch |err| switch (err) {
@@ -279,8 +279,9 @@ pub fn main(init: std.process.Init) !void {
.web_engine = web_engine.engine,
.web_layer_setting = web_layer_setting,
.cef_dir = web_engine.cef_dir,
- .signing = .{ .mode = signing, .identity = try flagValue(args, "--identity"), .entitlements = try flagValue(args, "--entitlements"), .team_id = try flagValue(args, "--team-id") },
+ .signing = .{ .mode = signing, .identity = try flagValue(args, "--identity"), .entitlements = try flagValue(args, "--entitlements"), .profile = try flagValue(args, "--notary-profile") },
.archive = archive,
+ .notarize = flagBool(args, "--notarize"),
.update_archive = flagBool(args, "--update-archive"),
.env_map = init.environ_map,
});
@@ -484,7 +485,7 @@ fn usage() void {
\\ doctor [--strict] [--manifest app.json] [--web-engine system|chromium] [--cef-dir path] [--cef-auto-install]
\\ validate [app.json|app.zon]
\\ bundle-assets [app.json|app.zon] [assets] [output]
- \\ package [--target macos|windows|linux|ios|android] [--output path] [--binary path] [--service-binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--team-id id] [--archive] [--update-archive]
+ \\ package [--target macos|windows|linux|ios|android] [--output path] [--binary path] [--service-binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--notarize --notary-profile name] [--archive] [--update-archive]
\\ dev [--manifest app.json] --binary path [--url http://127.0.0.1:5173/] [--command "npm run dev"] [--timeout-ms 30000]
\\ package-windows [--output path] [--binary path] [--service-binary path]
\\ package-linux [--output path] [--binary path] [--service-binary path]
@@ -974,7 +975,7 @@ fn positionalArg(args: []const []const u8) ?[]const u8 {
std.mem.eql(u8, arg, "--signing") or
std.mem.eql(u8, arg, "--identity") or
std.mem.eql(u8, arg, "--entitlements") or
- std.mem.eql(u8, arg, "--team-id") or
+ std.mem.eql(u8, arg, "--notary-profile") or
std.mem.eql(u8, arg, "--command") or
std.mem.eql(u8, arg, "--url") or
std.mem.eql(u8, arg, "--timeout-ms") or