@rightkit/release 0.2.137 → 0.2.139

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.
package/README.md CHANGED
@@ -18,6 +18,7 @@ Signing and publication rules are in the workspace file `docs/rules/release-sign
18
18
  | Group | Subcommands |
19
19
  |---|---|
20
20
  | Build | `release`, `build`, `package` |
21
+ | Build (SwiftPM apps) | `swift-package` (`windows-stage`, `msix`, `mac-app`) (config schema 1: `windows.{exe,out,searchDirs,include,dependencies,publisher,assets}`, `mac.{executable,bundleId,out,frameworks,resources,entitlements}`) |
21
22
  | Install | `install-app` |
22
23
  | Publish | `publish`, `upload`, `publish-update`, `github`, `mirror-root-artifact` |
23
24
  | Signing | `sign-windows`, `sign-updater`, `signing-agent`, `create-mac-updater` |
@@ -55,6 +56,16 @@ pnpm add -D @rightkit/release
55
56
  pnpm exec right-release deps --check
56
57
  ```
57
58
 
59
+ ## MSIX target and Swift app type
60
+
61
+ Swift (SwiftPM) apps are a first-class project type. `projectType: "swiftpm"` (config or target; auto-detected from a `Package.swift` with no Cargo manifest) replaces the cargo/rustc requirement with `swift`, and skips the Rust target-bridge, sccache and Perl checks. Tauri/Rust targets are unchanged. `makeappx` and `signtool` are required only when `targets.win.msix` is configured.
62
+
63
+ `targets.win` with `signingContract: "windows-msix-signed-v1"` packages an MSIX (schema in `msix-config.mjs`): `prePackage` builds and stages a flat payload (`right-release swift-package windows-stage`), then the msix step runs in the immutable order: verify the declared runtime-DLL inventory and PE import closure (fails closed), sign inner DLLs then EXEs through `sign-windows`, `makeappx pack`, sign the `.msix`, verify the signature and that the signer subject equals `Identity/@Publisher`, write the App Installer route, and write `msix-seal.json`. `msix.virtualization.fileSystemWrite|registryWrite` (`enabled|disabled`) are mandatory and emitted as `desktop6` manifest properties. List the `.msix` under `installer.artifacts` so it seals.
64
+
65
+ `updater.kind: "appinstaller"` (default remains the Tauri minisign flow) generates `msix.appInstaller.out` (https `uri`/`msixUri`); trust is the MSIX Authenticode signature, so no `.sig` is produced and `publish-update` records the sealed package digest.
66
+
67
+ macOS Swift apps use `right-release swift-package mac-app` (Info.plist from config, resources, frameworks, entitlements on the bundle only; nested code signed inside-out before the app).
68
+
58
69
  ## Known issues
59
70
 
60
71
  - `smol-toml` is pinned exactly. A consumer that adds another `smol-toml` gets a second copy (npm audit 2026-10-08, release finding).
package/build-release.mjs CHANGED
@@ -33,6 +33,8 @@ import { brokerManagedHost } from "./cargo-contract.mjs";
33
33
  import { resolveReleaseCacheMode } from "./release-cache-mode.mjs";
34
34
  import { resolveTargetRoot } from "./cargo-target.mjs";
35
35
  import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
36
+ import { collectSwiftToolVersions, planSwiftBuild } from "./swift-build-context.mjs";
37
+ import { packageCommandOptional, requiredBuildExecutables, resolveProjectType, targetUsesMsix } from "./project-type.mjs";
36
38
  import { buildPathPrefix, collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
37
39
  import { assertCleanSource } from "./source-gate.mjs";
38
40
  import { acquireHeavyWorkSlot, heavyCommandEnvironment, summarizePreBrokerWaits, terminateProcessTree } from "./heavy-command.mjs";
@@ -46,14 +48,14 @@ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
46
48
  // and test-only edits and would force a cold Rust rebuild of every app each time.
47
49
  // Cached target directories must not outlive changes to these signing helpers.
48
50
 
49
- const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "osascript-guard.mjs", "dmg-build.mjs", "dmg-layout.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
51
+ const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "osascript-guard.mjs", "dmg-build.mjs", "dmg-layout.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs", "msix-build.mjs", "msix-plan.mjs"];
50
52
  const PIPELINE_FINGERPRINT = createHash("sha256")
51
53
  .update(PIPELINE_FINGERPRINT_SOURCES.map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
52
54
  .digest("hex")
53
55
  .slice(0, 16);
54
56
 
55
57
  /** Assemble preflight inputs from the app's own files (mirrors release.mjs). */
56
- function buildPreflight({ config, configPath, appRoot, repoRoot, platform, nativeLayout }) {
58
+ function buildPreflight({ config, configPath, appRoot, repoRoot, platform, nativeLayout, target }) {
57
59
  let version;
58
60
  try {
59
61
  version = JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8")).version;
@@ -71,6 +73,8 @@ function buildPreflight({ config, configPath, appRoot, repoRoot, platform, nativ
71
73
  // reject a legitimate resume of an in-flight build.
72
74
  version: undefined,
73
75
  cargoLockPaths: nativeLayout?.lockCandidates ?? [],
76
+ projectType: resolveProjectType({ appRoot, config, target }),
77
+ msix: targetUsesMsix(target),
74
78
  });
75
79
  }
76
80
 
@@ -124,7 +128,7 @@ if (dryRun) {
124
128
  const config = (await import(`${pathToFileURL(configPath).href}?dry-run=${commit}`)).default;
125
129
  if (!config?.app || !config?.version) fail("release config must expose app and version");
126
130
  const target = config.targets?.[platform];
127
- if (!target?.package) fail(`${config.app} has no ${platform} package command`);
131
+ if (!target?.package && !packageCommandOptional(target, resolveProjectType({ appRoot, config, target }))) fail(`${config.app} has no ${platform} package command`);
128
132
  console.log(`right-release build dry-run: ${config.app} ${config.version} ${platform}`);
129
133
  console.log(`config: ${configPath}`);
130
134
  console.log(`commit: ${commit}`);
@@ -163,7 +167,8 @@ try {
163
167
  fail("app and version must be safe release-id components");
164
168
  }
165
169
  const target = config.targets?.[platform];
166
- if (!target?.package) fail(`${config.app} has no ${platform} package command`);
170
+ if (!target?.package && !packageCommandOptional(target, resolveProjectType({ appRoot, config, target }))) fail(`${config.app} has no ${platform} package command`);
171
+ const isSwiftBuild = resolveProjectType({ appRoot, config, target }) === "swiftpm";
167
172
  const nativeLayout = resolveNativeCargoLayout({ appRoot, config });
168
173
  if (platform === "win" && target.signingContract === "windows-raw-exe-authenticode-before-nsis-v1") {
169
174
  assertNsisInPlaceUpgradeContract(appRoot, target.nsisUpgradeContract);
@@ -171,7 +176,7 @@ try {
171
176
  const { requiredInputs, buildInputs } = resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target });
172
177
  const dirtyInputs = dirtyBuildInputs(repoRoot, buildInputs, commit);
173
178
  if (dirtyInputs.length > 0) fail(`dirty files can change the packaged app; resolve them before release:\n${dirtyInputs.map((file) => `- ${file}`).join("\n")}`);
174
- const toolVersions = collectToolVersions(config.packageManager ?? "pnpm");
179
+ const toolVersions = isSwiftBuild ? collectSwiftToolVersions({ packageManager: config.packageManager, exec: commandOutput }) : collectToolVersions(config.packageManager ?? "pnpm");
175
180
  const inputHashes = hashInputs(requiredInputs);
176
181
  const releaseId = `${config.app}-${config.version}-${shortCommit}`;
177
182
  const platformDir = platform === "win" ? "windows" : "mac";
@@ -208,10 +213,12 @@ try {
208
213
  // shells take the Cache V2 branch, bridge resolved native target into release cache,
209
214
  // and then lose CARGO_TARGET_DIR to the broker's unconditional override — so the
210
215
  // app bundle landed in the broker workspace and packaging failed on an empty cache.
211
- const managedCargoTarget = brokerManagedHost(process.env) ? resolveTargetRoot(cargoToml) : null;
212
- const cacheIdentity = resolveSharedCacheIdentity({ cargoLockPath: cargoLock, cargoTomlPath: cargoToml, rustcVerbose: toolVersions.rustc, targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? toolVersions.rustHost, profile: target.profile ?? "release" });
216
+ const swiftPlan = isSwiftBuild ? planSwiftBuild({ appRoot, vaultRoot, platform, target, toolVersions }) : null;
217
+ // SwiftPM targets have no Cargo manifest: no broker cargo target, no Rust cache identity.
218
+ const managedCargoTarget = swiftPlan ? swiftPlan.managedCargoTarget : brokerManagedHost(process.env) ? resolveTargetRoot(cargoToml) : null;
219
+ const cacheIdentity = swiftPlan ? swiftPlan.identity : resolveSharedCacheIdentity({ cargoLockPath: cargoLock, cargoTomlPath: cargoToml, rustcVerbose: toolVersions.rustc, targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? toolVersions.rustHost, profile: target.profile ?? "release" });
213
220
  const cacheKey = hashFileText(JSON.stringify({ cache: cacheIdentity.fingerprint, signingIdentity })).slice(0, 16);
214
- const cacheModeRequested = resolveReleaseCacheMode({ requested: process.env.RIGHT_RELEASE_CACHE_MODE, platform, env: process.env });
221
+ const cacheModeRequested = swiftPlan ? swiftPlan.cacheMode : resolveReleaseCacheMode({ requested: process.env.RIGHT_RELEASE_CACHE_MODE, platform, env: process.env });
215
222
  // On a broker-managed host, Cache V2's lease/slot/prune/floor/mark chain is not
216
223
  // running against the directory that will actually hold build output — the
217
224
  // broker owns CARGO_TARGET_DIR unconditionally (see managedCargoTarget above).
@@ -233,7 +240,7 @@ try {
233
240
  // Preflight BEFORE any compile. These preconditions used to be discovered one
234
241
  // per build cycle, minutes deep, with errors that named a symptom instead of a
235
242
  // cause. Blocking problems stop here, in seconds, naming the fix.
236
- const preflight = buildPreflight({ config, configPath, appRoot, repoRoot: layout.repoRoot, platform, nativeLayout });
243
+ const preflight = buildPreflight({ config, configPath, appRoot, repoRoot: layout.repoRoot, platform, nativeLayout, target });
237
244
  const preflightBlockers = preflightFailures(preflight);
238
245
  if (preflightBlockers.length > 0) {
239
246
  fail(`preflight found ${preflightBlockers.length} blocking problem(s):\n${formatPreflight(preflightBlockers)}`);
@@ -252,7 +259,7 @@ try {
252
259
  // resolves to MSYS Perl, which cannot configure OpenSSL. Put the real Windows
253
260
  // Perl first so the build behaves the same from any shell.
254
261
  const pathPrefix = buildPathPrefix({ platform });
255
- const env = macosCodesignEnvironment({
262
+ const baseEnv = macosCodesignEnvironment({
256
263
  ...heavyCommandEnvironment(process.env),
257
264
  ...releaseEnvironment({ root: vaultRoot, cacheRoot: sharedCacheRoot, platform, architecture: cacheIdentity.architecture, app: config.app, cacheKey, kind: "release", appRoot, mode: cacheMode }),
258
265
  ...(pathPrefix ? { PATH: `${pathPrefix}${path.delimiter}${process.env.PATH ?? ""}` } : {}),
@@ -261,6 +268,7 @@ try {
261
268
  RIGHTKIT_MANAGED_ROOT: resolveManagedRoot({ env: process.env }),
262
269
  RIGHT_RELEASE_ARTIFACT_DIR: resolveArtifactDir({ app: config.app, version: config.version, managedRoot: resolveManagedRoot({ env: process.env }) }),
263
270
  });
271
+ const env = swiftPlan ? swiftPlan.sanitizeEnv(baseEnv) : baseEnv;
264
272
  if (managedCargoTarget) {
265
273
  for (const name of ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "SCCACHE_CACHE_SIZE", "RUSTFLAGS"]) {
266
274
  delete env[name];
@@ -302,7 +310,7 @@ try {
302
310
  // ownedRoot is the per-app cache parent holding every fingerprint dir, so a
303
311
  // link left by an earlier fingerprint is recognised as ours and repointed
304
312
  // instead of hard-stopping the build.
305
- const targetBridge = createTargetBridge({
313
+ const targetBridge = swiftPlan ? swiftPlan.targetBridge : createTargetBridge({
306
314
  link: targetLink,
307
315
  target: managedCargoTarget ?? env.CARGO_TARGET_DIR,
308
316
  ownedRoot: path.dirname(managedCargoTarget ?? env.CARGO_TARGET_DIR),
@@ -329,7 +337,7 @@ try {
329
337
  const volumes = inspectWriteVolumes({ repoRoot, vaultRoot, cacheRoot: sharedCacheRoot });
330
338
  assertWriteVolumeFloors({ volumes, policy, configuredRepoMinimumBytes: Math.max(Number(target.preflight?.minFreeGb ?? 25), 25) * 1024 ** 3 });
331
339
  } else assertDiskSpace(repoRoot, target.preflight?.minFreeGb ?? 20);
332
- assertExecutables(["git", config.packageManager ?? "pnpm", "cargo", "rustc", ...(target.preflight?.executables ?? [])]);
340
+ assertExecutables([...requiredBuildExecutables({ projectType: resolveProjectType({ appRoot, config, target }), packageManager: config.packageManager ?? "pnpm", usesPackageManager: Boolean(config.packageManager) || existsSync(path.join(appRoot, "package.json")) }), ...(target.preflight?.executables ?? [])]);
333
341
  for (const name of target.preflight?.env ?? []) if (!process.env[name]) fail(`missing required environment variable: ${name}`);
334
342
  for (const command of target.preflight?.commands ?? []) runChecked(command.cmd, command.args ?? [], path.resolve(appRoot, command.cwd ?? "."), env);
335
343
  if (buildRoot) mkdirSync(buildRoot, { recursive: true });
@@ -341,7 +349,7 @@ try {
341
349
  },
342
350
  prepare: async () => {
343
351
  throwIfInterrupted();
344
- const cacheTarget = managedCargoTarget ?? env.CARGO_TARGET_DIR;
352
+ const cacheTarget = swiftPlan ? swiftPlan.buildDir : managedCargoTarget ?? env.CARGO_TARGET_DIR;
345
353
  mkdirSync(cacheTarget, { recursive: true });
346
354
  // Broker-managed hosts route through the same target bridge as shared
347
355
  // mode even though cacheMode is forced to "broker" above: the naive
@@ -349,12 +357,13 @@ try {
349
357
  // directory, which is exactly the "corruption" misreading that nearly
350
358
  // cost ~5.9 GB of warm broker intermediates. The bridge's brokerManaged
351
359
  // flag already knows to leave a real directory untouched instead.
352
- if (cacheMode === "shared" || managedCargoTarget) targetBridge.ensure();
360
+ if (swiftPlan) { /* SwiftPM: no Rust target link */ }
361
+ else if (cacheMode === "shared" || managedCargoTarget) targetBridge.ensure();
353
362
  else if (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
354
363
  else if (realpathSync(targetLink) !== realpathSync(cacheTarget)) {
355
364
  fail(`primary checkout target exists and is not the release cache link: ${targetLink}`);
356
365
  }
357
- await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, env, cacheTarget, path.join(stateRoot, "stall-install.log"));
366
+ if (resolveProjectType({ appRoot, config, target }) !== "swiftpm" || config.packageManager) await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, env, cacheTarget, path.join(stateRoot, "stall-install.log"));
358
367
  },
359
368
  build: async () => {
360
369
  throwIfInterrupted();
@@ -363,9 +372,9 @@ try {
363
372
  [WORKER, "--config", configPath, "--platform", platform, "--no-upload", "--resume", ...(resumeFrom ? ["--resume-from", resumeFrom] : []), ...(skipChecks ? ["--skip-checks"] : [])],
364
373
  appRoot,
365
374
  env,
366
- [managedCargoTarget ?? env.CARGO_TARGET_DIR, path.join(appRoot, ".cache")],
375
+ swiftPlan ? swiftPlan.watchDirs : [managedCargoTarget ?? env.CARGO_TARGET_DIR, path.join(appRoot, ".cache")],
367
376
  path.join(stateRoot, "stall-build.log"),
368
- { livenessOwnerTimeoutMs: target.package.livenessOwnerTimeoutMs ?? null },
377
+ { livenessOwnerTimeoutMs: target.package?.livenessOwnerTimeoutMs ?? null },
369
378
  );
370
379
  checkpoint(stateRoot, "build_complete");
371
380
  checkpoint(stateRoot, "signed");
@@ -461,7 +470,7 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
461
470
  for (const artifact of target.installer?.artifacts ?? []) addSource(resolveArtifactPath(artifact.file), "installer");
462
471
  for (const artifact of target.updater?.artifacts ?? []) {
463
472
  addSource(resolveArtifactPath(artifact.file), "updater");
464
- addSource(resolveArtifactPath(artifact.signature), "updater-signature");
473
+ if (target.updater?.kind !== "appinstaller") addSource(resolveArtifactPath(artifact.signature), "updater-signature");
465
474
  }
466
475
  for (const file of sources.keys()) if (!existsSync(file)) fail(`cannot seal missing artifact: ${file}`);
467
476
  const temp = `${sealedDir}.tmp-${process.pid}`;
@@ -485,10 +494,10 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
485
494
  }
486
495
  for (const artifact of target.updater?.artifacts ?? []) {
487
496
  const source = resolveArtifactPath(artifact.file);
488
- const signatureSource = resolveArtifactPath(artifact.signature);
497
+ const signatureSource = target.updater?.kind === "appinstaller" ? null : resolveArtifactPath(artifact.signature);
489
498
  const patchKey = artifact.patchKey ?? artifact.key.replace("/updates/", "/installers/");
490
- routes.patch.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(signatureSource), bucket: "public", key: patchKey, platform: artifact.platform });
491
- routes.update.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(signatureSource), bucket: "private", key: artifact.key, platform: artifact.platform });
499
+ routes.patch.push({ role: "updater", name: sourceNames.get(source), signature: signatureSource ? sourceNames.get(signatureSource) : undefined, bucket: "public", key: patchKey, platform: artifact.platform });
500
+ routes.update.push({ role: "updater", name: sourceNames.get(source), signature: signatureSource ? sourceNames.get(signatureSource) : undefined, bucket: "private", key: artifact.key, platform: artifact.platform });
492
501
  }
493
502
  writeJson(path.join(temp, "hardening.json"), { schema: 1, passed: true, checkedAt: new Date().toISOString(), files: files.map((file) => file.sha256) });
494
503
  const manifest = {
package/cache-policy.mjs CHANGED
@@ -518,3 +518,17 @@ function isAbsoluteForPlatform(value) { return isWindowsAbsolute(value) || path.
518
518
  function resolveForPlatform(value) { return isWindowsAbsolute(value) ? path.win32.resolve(value) : path.posix.resolve(value); }
519
519
  function readRustcVerbose() { const result = spawnSync("rustc", ["-vV"], { encoding: "utf8", windowsHide: true }); if (result.status !== 0) throw new Error(`rustc -vV failed: ${(result.stderr || result.error?.message || `exit ${result.status}`).trim()}`); return result.stdout.trim(); }
520
520
  function cargoNativeFeatures(cargoTomlPath) { if (!cargoTomlPath || !existsSync(cargoTomlPath)) return []; return [...readFileSync(cargoTomlPath, "utf8").matchAll(/features\s*=\s*\[([^\]]+)\]/g)].flatMap((match) => match[1].match(/"([^"]+)"/g) ?? []).map((value) => value.slice(1, -1)).filter((value) => /sqlcipher|openssl/i.test(value)); }
521
+
522
+ /**
523
+ * SwiftPM counterpart of resolveSharedCacheIdentity: identity is the Swift toolchain, Package.resolved
524
+ * (dependency pins; absent for dependency-free packages), target architecture and profile. No Rust inputs.
525
+ */
526
+ export function resolveSwiftCacheIdentity({ swiftVersion, packageResolvedPath, architecture, profile = "release" } = {}) {
527
+ if (!swiftVersion || !String(swiftVersion).trim()) throw new Error("swift --version did not report a toolchain");
528
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(architecture ?? "")) throw new Error("invalid Swift target architecture");
529
+ const resolved = packageResolvedPath && existsSync(packageResolvedPath) ? readFileSync(packageResolvedPath, "utf8").replace(/\r\n?/g, "\n") : null;
530
+ const packageResolvedSha256 = resolved === null ? "none" : sha256(resolved);
531
+ const swift = String(swiftVersion).trim();
532
+ return { projectType: "swiftpm", packageResolvedSha256, swift, target: architecture, architecture, profile, features: [],
533
+ fingerprint: sha256(JSON.stringify({ projectType: "swiftpm", packageResolvedSha256, swift, architecture, profile })).slice(0, 16) };
534
+ }
@@ -26,6 +26,7 @@ const commands = new Map([
26
26
  ["ownership", "ownership-command.mjs"],
27
27
  ["ownership-approval", "ownership-approval-command.mjs"],
28
28
  ["ownership-report", "ownership-report-command.mjs"],
29
+ ["swift-package", "swift-package-command.mjs"],
29
30
  ]);
30
31
 
31
32
  const args = process.argv.slice(2);
@@ -36,7 +37,7 @@ if (first === "dev") {
36
37
  process.exitCode = 1;
37
38
  });
38
39
  } else {
39
- const githubBuildCommands = new Set(["release", "build", "package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target"]);
40
+ const githubBuildCommands = new Set(["release", "build", "package", "swift-package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target"]);
40
41
 
41
42
  try {
42
43
  if (githubBuildCommands.has(first)
@@ -178,6 +179,8 @@ Commands:
178
179
  Resume macOS at prepackage, sign, package, post-sign, or harden
179
180
  dev --platform win|mac [--config <path>] [--dry-run] [--build-only]
180
181
  Incremental unsigned build & optional local install
182
+ swift-package <windows-stage|msix|mac-app> --config <swift-package.json> [--dry-run] [--no-sign] [--sign] [--notarize] [--profile <name>]
183
+ Stage SwiftPM app; build+sign MSIX (Windows) or assemble+sign+notarize .app (macOS); never uploads
181
184
  upload --release <id> --platform mac|win --tier patch|update
182
185
  Upload one sealed release; never builds
183
186
  publish-update --release <id> --platform mac|win --tier patch|update
@@ -132,7 +132,7 @@ function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runne
132
132
  const rows = parseTree(runGit(root, ["ls-tree", "-r", "-z", "--full-tree", tree], { encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env }), objectFormat);
133
133
  const wanted = new Set(requiredPaths.filter(validatePath));
134
134
  const shouldRead = (row) => row.type === "blob" && row.mode !== "120000" && (
135
- wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json", "Cargo.lock", "pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock"].includes(path.posix.basename(row.rel)) ||
135
+ wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "Package.resolved", "ownership-exceptions.json", "Cargo.lock", "pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock"].includes(path.posix.basename(row.rel)) ||
136
136
  /\.(?:rs|ts|tsx|js|mjs|cjs|jsx|swift|m|mm|c|cc|cpp|h|py|go|kt|java|cs)$/i.test(row.rel)
137
137
  );
138
138
  const candidates = rows.filter(shouldRead);
package/development.mjs CHANGED
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
8
8
  import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
9
- import { acquireManagedRunLease, managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
9
+ import { acquireManagedRunLease, managedRuntimeClient, managedRuntimeCommand, runCargoSync } from "./managed-cargo.mjs";
10
10
  import { BUNDLE_MARKERS, bundleMarkerBuffer, readTauriBundleMarkers } from "./tauri-bundle-marker.mjs";
11
11
  import { installPortableDevelopment } from "./portable-development.mjs";
12
12
  import { signMacosDevelopmentBundle } from "./development-signing.mjs";
@@ -213,7 +213,7 @@ export function resolveCommand(command, args, { root, env = process.env } = {})
213
213
  if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
214
214
  const socket = discoverBrokerSocket(env);
215
215
  const childEnv = !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : undefined;
216
- return { command: process.execPath, args: [client, ...args], ...(childEnv ? { env: childEnv } : {}) };
216
+ return { ...managedRuntimeCommand(client, args), ...(childEnv ? { env: childEnv } : {}) };
217
217
  }
218
218
  return { command, args };
219
219
  }
@@ -24,7 +24,7 @@ const CACHE_TTL_MS = 60 * 60 * 1000;
24
24
  const CONCURRENCY = 8;
25
25
  const REQUEST_TIMEOUT_MS = 15_000;
26
26
  const CRATES_API_INTERVAL_MS = 1000;
27
- const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/bogusyogi/claude)";
27
+ const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/bogusyogi/rightkit)";
28
28
  const EXCLUDED_PREFIX = "build-control-";
29
29
  const OWNER_PROBE_CRATE = "rightkit-fs";
30
30
  const EXTRA_ACCOUNT_CRATES = Object.freeze(["agent-room-core"]);
package/managed-cargo.mjs CHANGED
@@ -7,13 +7,19 @@ import { spawnSync } from "./background-process.mjs";
7
7
  export function managedRuntimeClient(env = process.env, platform = process.platform, exists = existsSync) {
8
8
  const roots = managedControlCandidates({ env, platform });
9
9
  const paths = platform === 'win32' ? path.win32 : path.posix;
10
+ // runtime-current ships the native `rustright` client.
10
11
  for (const root of roots) {
11
- const client = paths.join(root, "runtime-current", "build-control-clients", "src", "rightkit.mjs");
12
+ const client = paths.join(root, "runtime-current", platform === 'win32' ? "rustright.exe" : "rustright");
12
13
  if (exists(client)) return client;
13
14
  }
14
15
  return undefined;
15
16
  }
16
17
 
18
+ /** Command line that runs the managed client: the native rustright client runs directly. */
19
+ export function managedRuntimeCommand(client, args) {
20
+ return { command: client, args };
21
+ }
22
+
17
23
  /** Hold a client-run lease that blocks broker runtime cutover until release() (see
18
24
  * proveDevelopmentRunsDrained). No managed runtime → no lease; lease I/O failure warns and proceeds. */
19
25
  export function acquireManagedRunLease({ command, env = process.env, platform = process.platform, exists = existsSync, pid = process.pid } = {}) {
@@ -43,13 +49,17 @@ export function runCargoSync(args, options = {}, { platform = process.platform,
43
49
  const socket = discoverBrokerSocket(env, { platform, exists });
44
50
  if (platform === 'darwin' && (!socket || !exists(socket))) return unavailable('RightKit broker socket is unavailable');
45
51
  const childEnv = socket ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : env;
46
- const result = spawn(process.execPath, [client, "cargo", ...args], { ...options, env: childEnv });
52
+ const invocation = managedRuntimeCommand(client, ["cargo", ...args]);
53
+ const result = spawn(invocation.command, invocation.args, { ...options, env: childEnv });
47
54
  if (platform === 'darwin' && result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) {
48
55
  return { ...result, error: unavailable(String(result.stderr).trim()).error };
49
56
  }
50
57
  return result;
51
58
  }
52
- if (platform === 'darwin' || (managedHost(env) && platform === 'win32')) return unavailable('installed RightKit runtime client is unavailable for managed Cargo metadata');
59
+ // The Mac never falls back to direct Cargo, except on GitHub Actions runners, which have no
60
+ // RightKit runtime and use the toolchain's cargo (release-candidate signing jobs).
61
+ const hostedCi = env.GITHUB_ACTIONS === 'true' && !managedHost(env);
62
+ if ((platform === 'darwin' && !hostedCi) || (managedHost(env) && platform === 'win32')) return unavailable('installed RightKit runtime client is unavailable for managed Cargo metadata');
53
63
  return spawn("cargo", args, options);
54
64
  }
55
65
 
@@ -0,0 +1,24 @@
1
+ // App Installer (.appinstaller) update route for MSIX packages. Trust comes from the
2
+ // MSIX Authenticode signature (publisher must match the manifest); no separate updater key.
3
+
4
+ const xml = (value) => String(value).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]);
5
+ const fail = (message) => Object.assign(new Error(message), { code: "APPINSTALLER_INVALID" });
6
+
7
+ export function generateAppInstaller(o) {
8
+ const errors = [];
9
+ for (const key of ["uri", "msixUri"]) { try { if (new URL(o[key]).protocol !== "https:") throw 0; } catch { errors.push(`${key} must be an https URL`); } }
10
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(o.version ?? "")) errors.push("version must be four numeric parts");
11
+ if (!["x64", "arm64"].includes(o.architecture)) errors.push("architecture must be x64 or arm64");
12
+ if (!o.name || !o.publisher) errors.push("name and publisher are required");
13
+ const hours = o.hoursBetweenUpdateChecks ?? 24;
14
+ if (!Number.isInteger(hours) || hours < 0 || hours > 255) errors.push("hoursBetweenUpdateChecks must be an integer 0-255");
15
+ if (errors.length) throw fail(errors.join("; "));
16
+ return `<?xml version="1.0" encoding="utf-8"?>
17
+ <AppInstaller xmlns="http://schemas.microsoft.com/appx/appinstaller/2018" Version="${xml(o.version)}" Uri="${xml(o.uri)}">
18
+ <MainPackage Name="${xml(o.name)}" Publisher="${xml(o.publisher)}" Version="${xml(o.version)}" ProcessorArchitecture="${o.architecture}" Uri="${xml(o.msixUri)}" />
19
+ <UpdateSettings>
20
+ <OnLaunch HoursBetweenUpdateChecks="${hours}" ShowPrompt="${o.showPrompt ? "true" : "false"}" UpdateBlocksActivation="${o.updateBlocksActivation ? "true" : "false"}" />
21
+ </UpdateSettings>
22
+ </AppInstaller>
23
+ `;
24
+ }
package/msix-build.mjs ADDED
@@ -0,0 +1,164 @@
1
+ // Windows MSIX packaging for staged desktop apps (SwiftPM exe + app-local DLLs).
2
+ // Order mirrors the Windows rule in docs/rules/release-signing.md: sign raw PE
3
+ // files first, pack the exact signed bytes, then sign the package. All signing
4
+ // goes through sign-windows.mjs (provisioned Azure Artifact Signing); this module
5
+ // never touches credentials and never authenticates anything itself.
6
+
7
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { spawnSync } from "./background-process.mjs";
11
+ import { findWindowsSdkTool } from "./preflight.mjs";
12
+ import { generateAppInstaller } from "./msix-appinstaller.mjs";
13
+ import { assertPublisherMatchesSigner, planMsixSigning, verifyPayloadInventory } from "./msix-plan.mjs";
14
+ import { createHash } from "node:crypto";
15
+
16
+ const fail = (code, message) => Object.assign(new Error(message), { code });
17
+ const here = path.dirname(fileURLToPath(import.meta.url));
18
+ export const MSIX_SIGNER_SUBJECT = "CN=Damned Ventures LLC";
19
+
20
+ const xml = (value) => String(value).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&apos;" })[c]);
21
+ const PNG = Buffer.from("89504e470d0a1a0a", "hex");
22
+
23
+ export function validateMsixIdentity(o) {
24
+ const errors = [];
25
+ if (!/^[-.A-Za-z0-9]{3,50}$/.test(o.name ?? "")) errors.push("name must be 3-50 chars of letters, digits, '-' or '.'");
26
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(o.version ?? "") || o.version.split(".").some((n) => Number(n) > 65535)) errors.push("version must be four numeric parts <=65535");
27
+ if (!["x64", "arm64"].includes(o.architecture)) errors.push("architecture must be x64 or arm64");
28
+ if (!/\bCN=/.test(o.publisher ?? "")) errors.push("publisher must be a distinguished name containing CN=");
29
+ else if (!o.publisher.includes(MSIX_SIGNER_SUBJECT)) errors.push(`publisher must contain ${MSIX_SIGNER_SUBJECT} (the provisioned signing subject); confirm the full DN natively`);
30
+ if (!o.displayName || !o.publisherDisplayName) errors.push("displayName and publisherDisplayName are required");
31
+ if (!/\.exe$/i.test(o.executable ?? "") || /[\\/]/.test(o.executable ?? "")) errors.push("executable must be a flat .exe name inside the package");
32
+ if (errors.length) throw fail("MSIX_INVALID_IDENTITY", errors.join("; "));
33
+ }
34
+
35
+ export function generateAppxManifest(o) {
36
+ validateMsixIdentity(o);
37
+ const deps = (o.dependencies ?? []).filter((d) => d.minVersion).map((d) =>
38
+ ` <PackageDependency Name="${xml(d.name)}" MinVersion="${xml(d.minVersion)}" Publisher="${xml(d.publisher ?? "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US")}" />`);
39
+ const v = o.virtualization;
40
+ for (const key of ["fileSystemWrite", "registryWrite"]) {
41
+ if (v?.[key] !== undefined && !["enabled", "disabled"].includes(v[key])) throw fail("MSIX_INVALID_IDENTITY", `virtualization.${key} must be "enabled" or "disabled"`);
42
+ }
43
+ const virt = v ? [["fileSystemWrite", "FileSystemWriteVirtualization"], ["registryWrite", "RegistryWriteVirtualization"]]
44
+ .filter(([k]) => v[k]).map(([k, el]) => `\n <desktop6:${el}>${v[k]}</desktop6:${el}>`).join("") : "";
45
+ // Opting out of virtualization is a restricted capability.
46
+ const unvirtualized = v && (v.fileSystemWrite === "disabled" || v.registryWrite === "disabled") ? '\n <rescap:Capability Name="unvirtualizedResources" />' : "";
47
+ return `<?xml version="1.0" encoding="utf-8"?>
48
+ <Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
49
+ xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
50
+ xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
51
+ xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
52
+ IgnorableNamespaces="uap rescap">
53
+ <Identity Name="${xml(o.name)}" Publisher="${xml(o.publisher)}" Version="${xml(o.version)}" ProcessorArchitecture="${o.architecture}" />
54
+ <Properties>
55
+ <DisplayName>${xml(o.displayName)}</DisplayName>
56
+ <PublisherDisplayName>${xml(o.publisherDisplayName)}</PublisherDisplayName>
57
+ <Logo>Assets\\StoreLogo.png</Logo>${virt}
58
+ </Properties>
59
+ <Dependencies>
60
+ <TargetDeviceFamily Name="Windows.Desktop" MinVersion="${xml(o.minWindowsVersion ?? "10.0.19041.0")}" MaxVersionTested="${xml(o.maxWindowsVersionTested ?? "10.0.26100.0")}" />
61
+ ${deps.join("\n")}${deps.length ? "\n" : ""} </Dependencies>
62
+ <Resources><Resource Language="${xml(o.language ?? "en-us")}" /></Resources>
63
+ <Applications>
64
+ <Application Id="App" Executable="${xml(o.executable)}" EntryPoint="Windows.FullTrustApplication">
65
+ <uap:VisualElements DisplayName="${xml(o.displayName)}" Description="${xml(o.description ?? o.displayName)}" BackgroundColor="transparent"
66
+ Square150x150Logo="Assets\\Square150x150Logo.png" Square44x44Logo="Assets\\Square44x44Logo.png" />
67
+ </Application>
68
+ </Applications>
69
+ <Capabilities>
70
+ <rescap:Capability Name="runFullTrust" />${unvirtualized}${(o.capabilities ?? []).map((c) => `\n <Capability Name="${xml(c)}" />`).join("")}
71
+ </Capabilities>
72
+ </Package>
73
+ `;
74
+ }
75
+
76
+ const ASSETS = { storeLogo: "StoreLogo.png", square150: "Square150x150Logo.png", square44: "Square44x44Logo.png" };
77
+
78
+ /** Copy a staged directory plus assets and manifest into a package root. */
79
+ export function assembleMsixRoot({ stageDir, root, manifest, assets }) {
80
+ stageDir = path.resolve(stageDir); root = path.resolve(root);
81
+ if (!existsSync(path.join(stageDir, manifest.executable))) throw fail("MSIX_MISSING_EXECUTABLE", `executable not in staged directory: ${manifest.executable}`);
82
+ rmSync(root, { recursive: true, force: true });
83
+ mkdirSync(path.join(root, "Assets"), { recursive: true });
84
+ for (const entry of readdirSync(stageDir, { withFileTypes: true })) {
85
+ if (entry.isFile()) copyFileSync(path.join(stageDir, entry.name), path.join(root, entry.name));
86
+ else throw fail("MSIX_NESTED_STAGE", `staged directory must be flat; found ${entry.name}/ (use include with flat files)`);
87
+ }
88
+ for (const [key, target] of Object.entries(ASSETS)) {
89
+ const source = assets?.[key] && path.resolve(assets[key]);
90
+ if (!source || !existsSync(source)) throw fail("MSIX_MISSING_ASSET", `assets.${key} (PNG) is required: ${assets?.[key] ?? "unset"}`);
91
+ if (!readFileSync(source).subarray(0, 8).equals(PNG)) throw fail("MSIX_MISSING_ASSET", `assets.${key} is not a PNG: ${source}`);
92
+ copyFileSync(source, path.join(root, "Assets", target));
93
+ }
94
+ writeFileSync(path.join(root, "AppxManifest.xml"), generateAppxManifest(manifest));
95
+ return root;
96
+ }
97
+
98
+ /** PE files the package ships; these get Authenticode before packing. */
99
+ export function peFilesIn(root) {
100
+ return readdirSync(root).filter((name) => /\.(exe|dll)$/i.test(name)).sort().map((name) => path.join(root, name));
101
+ }
102
+
103
+ const sha256 = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
104
+
105
+ function readSignerSubject(file, run) {
106
+ const r = run("powershell", ["-NoProfile", "-Command", `(Get-AuthenticodeSignature -LiteralPath '${file.replace(/'/g, "''")}').SignerCertificate.Subject`], { encoding: "utf8", windowsHide: true });
107
+ return r.status === 0 ? String(r.stdout ?? "").trim() : "";
108
+ }
109
+
110
+ /**
111
+ * Pack + sign. `run` is injectable for tests. Ordered per planMsixSigning():
112
+ * assemble, verify-inventory, sign-pe (DLLs then EXEs, raw bytes), pack, sign-msix, verify-msix,
113
+ * verify-publisher, appinstaller, seal. Windows-only unless dryRun.
114
+ * options.runtime = { dlls, systemDlls, dependencies } (declared inventory, verified fail-closed);
115
+ * options.appInstaller = { uri, msixUri, out, ... } generates the update route after the package is signed.
116
+ */
117
+ export function buildMsix({ stageDir, out, workDir, manifest, assets, runtime, appInstaller, sign = true, dryRun = false, receiptDir, peReceipt, msixReceipt },
118
+ { platform = process.platform, run = spawnSync, findTool = findWindowsSdkTool, env = process.env, log = (e) => console.error(JSON.stringify(e)), readSubject = (file) => readSignerSubject(file, run) } = {}) {
119
+ out = path.resolve(out);
120
+ if (!out.endsWith(".msix")) throw fail("MSIX_INVALID_OPTIONS", "out must end with .msix");
121
+ validateMsixIdentity(manifest);
122
+ workDir = path.resolve(workDir ?? `${out}.root`);
123
+ const receipts = path.resolve(receiptDir ?? path.dirname(out));
124
+ const planFor = (files) => planMsixSigning({ files, sign, appInstaller: Boolean(appInstaller) });
125
+ const staged = existsSync(path.resolve(stageDir)) ? readdirSync(path.resolve(stageDir)) : [];
126
+ const steps = planFor(staged.filter((n) => /\.(exe|dll)$/i.test(n))).map((p) => p.step);
127
+ if (dryRun) { log({ event: "msix-dry-run", out, steps }); return { out, steps, dryRun: true }; }
128
+ if (platform !== "win32") throw fail("MSIX_NATIVE_HOST_REQUIRED", "MSIX packing and signing must run on Windows");
129
+ const makeappx = env.MAKEAPPX_PATH || findTool("makeappx.exe")?.toolPath;
130
+ if (!makeappx) throw fail("MSIX_TOOL_MISSING", "makeappx.exe not found; install the Windows SDK or set MAKEAPPX_PATH");
131
+ const checked = (cmd, args) => {
132
+ const r = run(cmd, args, { encoding: "utf8", windowsHide: true, timeout: 600000 });
133
+ if (r.error || r.status !== 0) throw fail("MSIX_COMMAND_FAILED", `${path.basename(cmd)} ${args[0] ?? ""} failed: ${r.error?.message ?? r.stderr ?? r.stdout ?? r.status}`);
134
+ return r;
135
+ };
136
+ const signer = (files, receipt) => checked(process.execPath, [path.join(here, "sign-windows.mjs"), "--receipt", receipt, ...files]);
137
+ const root = assembleMsixRoot({ stageDir, root: workDir, manifest, assets });
138
+ const inventory = runtime ? verifyPayloadInventory({ root, inventory: runtime, architecture: manifest.architecture }) : null;
139
+ mkdirSync(receipts, { recursive: true });
140
+ const ordered = planFor(readdirSync(root).filter((n) => /\.(exe|dll)$/i.test(n))).find((p) => p.step === "sign-pe")?.files ?? [];
141
+ if (sign) signer(ordered.map((f) => path.join(root, path.basename(f))), peReceipt ?? path.join(receipts, "msix-pe-sign.receipt.json"));
142
+ mkdirSync(path.dirname(out), { recursive: true });
143
+ rmSync(out, { force: true });
144
+ checked(makeappx, ["pack", "/d", root, "/p", out, "/o"]);
145
+ if (!existsSync(out) || statSync(out).size === 0) throw fail("MSIX_PACK_EMPTY", `makeappx produced no package: ${out}`);
146
+ if (sign) {
147
+ signer([out], msixReceipt ?? path.join(receipts, "msix-sign.receipt.json"));
148
+ checked(process.execPath, [path.join(here, "sign-windows.mjs"), "--verify-only", out]);
149
+ assertPublisherMatchesSigner(manifest.publisher, readSubject(out));
150
+ }
151
+ let appInstallerFile = null;
152
+ if (appInstaller) {
153
+ appInstallerFile = path.resolve(appInstaller.out);
154
+ mkdirSync(path.dirname(appInstallerFile), { recursive: true });
155
+ writeFileSync(appInstallerFile, generateAppInstaller({ ...appInstaller, name: manifest.name, publisher: manifest.publisher, version: manifest.version, architecture: manifest.architecture }));
156
+ }
157
+ const seal = { schema: 1, msix: { file: out, sha256: sha256(out), sizeBytes: statSync(out).size }, signed: sign, identity: { name: manifest.name, version: manifest.version, publisher: manifest.publisher, architecture: manifest.architecture },
158
+ payload: readdirSync(root, { recursive: true }).map((n) => String(n).split(path.sep).join("/")).filter((n) => statSync(path.join(root, n)).isFile()).sort().map((n) => ({ name: n, sha256: sha256(path.join(root, n)) })), inventory,
159
+ appInstaller: appInstallerFile && { file: appInstallerFile, sha256: sha256(appInstallerFile) } };
160
+ const sealFile = path.join(receipts, "msix-seal.json");
161
+ writeFileSync(sealFile, `${JSON.stringify(seal, null, 2)}\n`);
162
+ log({ event: "msix-built", out, signed: sign });
163
+ return { out, steps, root, signed: sign, seal: sealFile, appInstaller: appInstallerFile };
164
+ }
@@ -0,0 +1,70 @@
1
+ // `msix` target schema for right-release configs (targets.win.msix) and the
2
+ // `windows-msix-signed-v1` signing contract. Pure validation; no I/O.
3
+ //
4
+ // targets.win = {
5
+ // signed: true, signingContract: "windows-msix-signed-v1",
6
+ // projectType: "swiftpm", // optional; auto-detected from Package.swift
7
+ // prePackage: { cmd, args }, // SwiftPM build + stage (swift-package windows-stage)
8
+ // msix: {
9
+ // stageDir, out, // flat payload dir produced by prePackage; .msix path
10
+ // identity: { name, publisher, publisherDisplayName, displayName, description?, version, architecture, executable },
11
+ // capabilities: ["internetClient"], minWindowsVersion?, maxWindowsVersionTested?,
12
+ // virtualization: { fileSystemWrite: "enabled|disabled", registryWrite: "enabled|disabled" }, // explicit, required
13
+ // assets: { storeLogo, square150, square44 },
14
+ // runtime: { dlls: ["swiftCore.dll", ...], systemDlls?: [], dependencies?: [{ name, minVersion, provides: [] }] },
15
+ // appInstaller?: { uri, msixUri, out, hoursBetweenUpdateChecks?, showPrompt?, updateBlocksActivation? },
16
+ // },
17
+ // updater?: { kind: "tauri" | "appinstaller", artifacts: [...] }, // tauri stays the default
18
+ // installer: { artifacts: [{ file: "<out>.msix", key }] },
19
+ // }
20
+
21
+ export const MSIX_SIGNING_CONTRACT = "windows-msix-signed-v1";
22
+ export const UPDATER_KINDS = Object.freeze(["tauri", "appinstaller"]);
23
+ export const VIRTUALIZATION_VALUES = Object.freeze(["enabled", "disabled"]);
24
+
25
+ export const isMsixTarget = (target) => target?.signingContract === MSIX_SIGNING_CONTRACT;
26
+ export const updaterKind = (target) => target?.updater?.kind ?? "tauri";
27
+
28
+ const isHttps = (value) => { try { return new URL(value).protocol === "https:"; } catch { return false; } };
29
+
30
+ /** Returns an array of human-readable problems (empty when valid). */
31
+ export function validateMsixTarget(target, app = "app") {
32
+ const e = [];
33
+ const at = (message) => e.push(`${app} win msix: ${message}`);
34
+ if (!isMsixTarget(target)) at(`signingContract must be ${MSIX_SIGNING_CONTRACT}`);
35
+ const m = target?.msix;
36
+ if (!m || typeof m !== "object") { at("msix block is required"); return e; }
37
+ if (!target.prePackage?.cmd) at("prePackage is required (SwiftPM build + payload staging)");
38
+ if (target.sign?.prePackageFiles?.length || target.sign?.files?.length) at("omit sign.prePackageFiles/sign.files: the msix step signs payload PE files, then the package");
39
+ if (!m.stageDir) at("msix.stageDir is required");
40
+ if (!/\.msix$/i.test(m.out ?? "")) at("msix.out must end with .msix");
41
+ const id = m.identity ?? {};
42
+ for (const key of ["name", "publisher", "publisherDisplayName", "displayName", "version", "architecture", "executable"]) if (!id[key]) at(`msix.identity.${key} is required`);
43
+ const v = m.virtualization ?? {};
44
+ for (const key of ["fileSystemWrite", "registryWrite"]) {
45
+ if (!VIRTUALIZATION_VALUES.includes(v[key])) at(`msix.virtualization.${key} must be explicitly "enabled" or "disabled"`);
46
+ }
47
+ for (const key of ["storeLogo", "square150", "square44"]) if (!m.assets?.[key]) at(`msix.assets.${key} (PNG) is required`);
48
+ const dlls = m.runtime?.dlls;
49
+ if (!Array.isArray(dlls)) at("msix.runtime.dlls (explicit runtime dependency inventory; use [] only for a dependency-free app) is required");
50
+ else if (dlls.some((d) => !/\.dll$/i.test(d) || /[\\/]/.test(d))) at("msix.runtime.dlls entries must be flat .dll names");
51
+ const kind = updaterKind(target);
52
+ if (!UPDATER_KINDS.includes(kind)) at(`updater.kind must be one of ${UPDATER_KINDS.join(", ")}`);
53
+ if (kind === "appinstaller") {
54
+ const a = m.appInstaller;
55
+ if (!a) at("updater.kind appinstaller requires msix.appInstaller");
56
+ else {
57
+ if (!isHttps(a.uri)) at("msix.appInstaller.uri must be an https URL");
58
+ if (!isHttps(a.msixUri)) at("msix.appInstaller.msixUri must be an https URL");
59
+ if (!/\.appinstaller$/i.test(a.out ?? "")) at("msix.appInstaller.out must end with .appinstaller");
60
+ }
61
+ }
62
+ const outName = String(m.out ?? "").split(/[\\/]/).pop();
63
+ if (outName && !(target.installer?.artifacts ?? []).some((a) => String(a.file).split(/[\\/]/).pop() === outName)) at("installer.artifacts must list the .msix so it is sealed");
64
+ return e;
65
+ }
66
+
67
+ export function assertValidMsixTarget(target, app) {
68
+ const errors = validateMsixTarget(target, app);
69
+ if (errors.length) throw Object.assign(new Error(errors.join("; ")), { code: "MSIX_CONFIG_INVALID" });
70
+ }