aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-11 17:47:42 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-11 17:51:51 +0200
commitedbff1768054afa80efda721cd1011b29c7fe355 (patch)
tree4ae3d27877bd08e92cc0981b80bcc9502e82bff5
parentd4935aa2a28fcbab8c3556e3532e53667092701e (diff)
downloadmeshbay-edbff1768054afa80efda721cd1011b29c7fe355.tar.gz
feat(packaging): an MSIX target for Microsoft Store submission
Store certification of the NSIS "MSI/EXE" submission failed on three checks (silent-install verification, Add/Remove Programs entry, bundleware check) -- traced and reproduced live to one cause: SmartScreen blocks an unsigned, internet-downloaded installer at the shell layer before Microsoft's own unattended validation bot ever gets to run it. MSIX sidesteps this class of failure entirely: submitted through the Store's native pipeline, there is no browser-download-then-launch step for SmartScreen to intercept, and Microsoft signs the package itself at publish time -- free, and specific to this submission type (Trusted Signing remains a paid service for the MSI/EXE path). Full plan and findings: C:\Users\admin\devel\msix-installer.md (out of repo). electron-builder.msix.yml carries the same bundle as Full (node runtime, ffmpeg, both service scripts) -- an AppX/MSIX install never elevates, by design, but that changes only *when* the two elevated operations can run, not whether the daemon ships. No main.js changes were needed: the on-demand elevation path for service-mode (winElevateServiceMode(), driven from the Node page) already existed for a different reason and depends only on service-mode.ps1 being present as an extraResource, true for any packaged Windows target. identityName/publisher/publisherDisplayName are the real values from Partner Center's app-identity reservation, not placeholders. build-win-msix.ps1 points electron-builder at the system Windows 10 SDK (auto-detected) instead of letting it download its own bundled copy -- that download's 7z extraction creates symlinks this target never uses and fails without SeCreateSymbolicLinkPrivilege, reproduced on this machine. build/appx/ carries the four tile images the AppX target requires regardless of showNameOnTiles, generated once from the existing app icon (see that directory's README) since the system-SDK redirect has no vendor samples to fall back to. build/appx-extensions.xml declares windows.startupTask by hand rather than via electron-builder's addAutoLaunchExtension, which always targets the Electron shell -- this points at the bundled node binary instead, matching what "starts at sign in" already means for Full. Verified live via a signed sideload install (self-signed test cert, cleaned up after): the package installs and the app runs correctly. One finding worth carrying forward -- the declared network capabilities (internetClientServer, privateNetworkClientServer) do not create any firewall exemption for this app, most likely because automatic capability-based exemption is an AppContainer-sandbox property and this app deliberately runs full-trust, outside any sandbox. Not a regression: no install-time elevation was possible either way, so the cost is the same one-time OS firewall prompt firewall.ps1's own header already documents as its fallback today. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-rw-r--r--.gitignore3
-rw-r--r--packages/meshbay-client/build/appx-extensions.xml3
-rw-r--r--packages/meshbay-client/build/appx/README.md44
-rw-r--r--packages/meshbay-client/build/appx/Square150x150Logo.pngbin0 -> 38507 bytes
-rw-r--r--packages/meshbay-client/build/appx/Square44x44Logo.pngbin0 -> 4258 bytes
-rw-r--r--packages/meshbay-client/build/appx/StoreLogo.pngbin0 -> 5388 bytes
-rw-r--r--packages/meshbay-client/build/appx/Wide310x150Logo.pngbin0 -> 32069 bytes
-rw-r--r--packages/meshbay-client/package.json5
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py204
-rw-r--r--packaging/win/build-win-msix.ps1134
-rw-r--r--packaging/win/electron-builder.msix.yml113
11 files changed, 499 insertions, 7 deletions
diff --git a/.gitignore b/.gitignore
index e1cbdf2..95e1550 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,9 @@ packages/meshbay-client/node-runtime/
# directories.output) -- the bare `dist/` rule above does not match this
# different name, so it needs its own line.
packages/meshbay-client/dist-light/
+# build-win-msix.ps1's own output dir (electron-builder.msix.yml
+# directories.output) -- same reasoning as dist-light/ above.
+packages/meshbay-client/dist-msix/
# electron-builder's buildResources dir — a real source (app icon), not a
# build artifact, despite living under a path the generic `build/` rule above
diff --git a/packages/meshbay-client/build/appx-extensions.xml b/packages/meshbay-client/build/appx-extensions.xml
new file mode 100644
index 0000000..1ee1cca
--- /dev/null
+++ b/packages/meshbay-client/build/appx-extensions.xml
@@ -0,0 +1,3 @@
+ <desktop:Extension Category="windows.startupTask" Executable="app\resources\node-runtime\meshbay-node.exe" EntryPoint="Windows.FullTrustApplication">
+ <desktop:StartupTask TaskId="MeshBayNodeStartup" Enabled="true" DisplayName="MeshBay Node" />
+ </desktop:Extension>
diff --git a/packages/meshbay-client/build/appx/README.md b/packages/meshbay-client/build/appx/README.md
new file mode 100644
index 0000000..ef47411
--- /dev/null
+++ b/packages/meshbay-client/build/appx/README.md
@@ -0,0 +1,44 @@
+# AppX/MSIX tile images
+
+Generated once from `../icon-square.png` (1024×1024), not by any build script
+— electron-builder's AppX target (`app-builder-lib/out/targets/AppxTarget.js`)
+requires these four regardless of `showNameOnTiles`, and looks for them here
+(`packager.getResource(undefined, "appx")` resolves to `build/appx/`) before
+falling back to its own vendor-bundled samples. Falling back was not an
+option here: this repo points `ELECTRON_BUILDER_WINDOWS_KITS_PATH` at the
+system Windows SDK instead of letting electron-builder download its own
+copy (see `packaging/win/build-win-msix.ps1`'s comment for why), and the
+system SDK has no `appxAssets` samples to fall back to at all.
+
+| File | Size | Source |
+|---|---|---|
+| `StoreLogo.png` | 50×50 | resized |
+| `Square44x44Logo.png` | 44×44 | resized |
+| `Square150x150Logo.png` | 150×150 | resized |
+| `Wide310x150Logo.png` | 310×150 | icon centered on a `#464646` canvas (the appx target's own default `backgroundColor`) |
+
+Regenerate with System.Drawing if the source icon changes:
+
+```powershell
+Add-Type -AssemblyName System.Drawing
+$img = [System.Drawing.Image]::FromFile("build\icon-square.png")
+function Resize-Square($image, $size, $outPath) {
+ $bmp = New-Object System.Drawing.Bitmap $size, $size
+ $g = [System.Drawing.Graphics]::FromImage($bmp)
+ $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
+ $g.DrawImage($image, 0, 0, $size, $size)
+ $bmp.Save($outPath, [System.Drawing.Imaging.ImageFormat]::Png)
+ $g.Dispose(); $bmp.Dispose()
+}
+Resize-Square $img 50 "build\appx\StoreLogo.png"
+Resize-Square $img 44 "build\appx\Square44x44Logo.png"
+Resize-Square $img 150 "build\appx\Square150x150Logo.png"
+
+$bmp = New-Object System.Drawing.Bitmap 310, 150
+$g = [System.Drawing.Graphics]::FromImage($bmp)
+$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
+$g.Clear([System.Drawing.Color]::FromArgb(255, 0x46, 0x46, 0x46))
+$g.DrawImage($img, 90, 10, 130, 130)
+$bmp.Save("build\appx\Wide310x150Logo.png", [System.Drawing.Imaging.ImageFormat]::Png)
+$g.Dispose(); $bmp.Dispose(); $img.Dispose()
+```
diff --git a/packages/meshbay-client/build/appx/Square150x150Logo.png b/packages/meshbay-client/build/appx/Square150x150Logo.png
new file mode 100644
index 0000000..cd64829
--- /dev/null
+++ b/packages/meshbay-client/build/appx/Square150x150Logo.png
Binary files differ
diff --git a/packages/meshbay-client/build/appx/Square44x44Logo.png b/packages/meshbay-client/build/appx/Square44x44Logo.png
new file mode 100644
index 0000000..d714c94
--- /dev/null
+++ b/packages/meshbay-client/build/appx/Square44x44Logo.png
Binary files differ
diff --git a/packages/meshbay-client/build/appx/StoreLogo.png b/packages/meshbay-client/build/appx/StoreLogo.png
new file mode 100644
index 0000000..2062b36
--- /dev/null
+++ b/packages/meshbay-client/build/appx/StoreLogo.png
Binary files differ
diff --git a/packages/meshbay-client/build/appx/Wide310x150Logo.png b/packages/meshbay-client/build/appx/Wide310x150Logo.png
new file mode 100644
index 0000000..0ca324d
--- /dev/null
+++ b/packages/meshbay-client/build/appx/Wide310x150Logo.png
Binary files differ
diff --git a/packages/meshbay-client/package.json b/packages/meshbay-client/package.json
index 614e6d6..6d8e711 100644
--- a/packages/meshbay-client/package.json
+++ b/packages/meshbay-client/package.json
@@ -1,7 +1,7 @@
{
"name": "meshbay-client",
"version": "0.13.0",
- "description": "MeshBay desktop client \u2014 the interface ships with the application, not from the hub",
+ "description": "MeshBay desktop client — the interface ships with the application, not from the hub",
"license": "AGPL-3.0-or-later",
"author": "MeshBay Team <team@meshbay.org>",
"homepage": "https://meshbay.org",
@@ -12,7 +12,8 @@
"sync-ui": "node scripts/sync-ui.js",
"dist": "bash ../../packaging/build/build-client.sh",
"dist:win": "powershell -NoProfile -ExecutionPolicy Bypass -File ../../packaging/win/build-win.ps1",
- "dist:win:light": "powershell -NoProfile -ExecutionPolicy Bypass -File ../../packaging/win/build-win-light.ps1"
+ "dist:win:light": "powershell -NoProfile -ExecutionPolicy Bypass -File ../../packaging/win/build-win-light.ps1",
+ "dist:win:msix": "powershell -NoProfile -ExecutionPolicy Bypass -File ../../packaging/win/build-win-msix.ps1"
},
"devDependencies": {
"electron": "^44.2.0",
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index b223156..2cc8f12 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -31,6 +31,13 @@ LIGHT_YML = WIN / "electron-builder.light.yml"
BUILD_WIN_LIGHT = WIN / "build-win-light.ps1"
BUILD_WIN_COMMON = WIN / "build-win-common.ps1"
+# The "MSIX" target: same feature set as Full, packaged for Microsoft Store
+# submission instead of NSIS. See C:\Users\admin\devel\msix-installer.md for
+# the plan this implements.
+MSIX_YML = WIN / "electron-builder.msix.yml"
+BUILD_WIN_MSIX = WIN / "build-win-msix.ps1"
+MSIX_EXTENSIONS_XML = CLIENT / "build" / "appx-extensions.xml"
+
# sync-ui.js copies this into CLIENT/ui/ verbatim -- read the source of
# truth, same as every other cross-package check in this file already does
# for meshbay-client's own src/.
@@ -746,10 +753,10 @@ def test_build_win_light_skips_the_node_runtime_step():
def test_build_orchestrators_share_the_common_steps_not_a_copy():
"""
Node check / npm ci / Electron bump / sync-ui must live in exactly one
- place (build-win-common.ps1), dot-sourced by both -- a copy would let
- the two drift the way the installer flow itself once did (the W3
- one-shot dialog bug). Both orchestrators must call the shared
- functions, neither may inline its own npm ci / Electron-bump logic.
+ place (build-win-common.ps1), dot-sourced by all three orchestrators --
+ a copy would let them drift the way the installer flow itself once did
+ (the W3 one-shot dialog bug). Every orchestrator must call the shared
+ functions, none may inline its own npm ci / Electron-bump logic.
"""
assert BUILD_WIN_COMMON.exists(), f"{BUILD_WIN_COMMON} is missing"
common = BUILD_WIN_COMMON.read_text(encoding="utf-8")
@@ -758,7 +765,9 @@ def test_build_orchestrators_share_the_common_steps_not_a_copy():
full = (WIN / "build-win.ps1").read_text(encoding="utf-8")
light = BUILD_WIN_LIGHT.read_text(encoding="utf-8")
- for src, name in ((full, "build-win.ps1"), (light, "build-win-light.ps1")):
+ msix = BUILD_WIN_MSIX.read_text(encoding="utf-8")
+ for src, name in ((full, "build-win.ps1"), (light, "build-win-light.ps1"),
+ (msix, "build-win-msix.ps1")):
assert '. (Join-Path $WinDir "build-win-common.ps1")' in src, (
f"{name} does not dot-source build-win-common.ps1")
for fn in ("Assert-NodeVersion", "Invoke-NpmCi", "Invoke-ElectronBump", "Invoke-SyncUi"):
@@ -908,3 +917,188 @@ def test_create_group_page_falls_back_when_no_node_is_bundled():
assert "platform.node.bundled()" in body
assert "platform.node.available && bundled" in body
assert "CreateGroupWizard" in body and "CreateGroupFormSimple" in body
+
+
+# ------------------------------------------------------------------------
+# The "MSIX" target: same feature set as Full, packaged for Microsoft Store
+# submission instead of NSIS. See C:\Users\admin\devel\msix-installer.md for
+# the plan. Unlike Light, this target keeps the node runtime and both
+# service scripts -- what changes is packaging format, not what ships.
+# Weak, text-reading evidence throughout, same reasoning as the rest of
+# this file: there is no electron-builder/appx runner here either.
+# ------------------------------------------------------------------------
+
+def test_msix_config_is_standalone_and_keeps_the_full_bundle():
+ """
+ Unlike Light, MSIX ships the same node-runtime/ffmpeg/service scripts as
+ Full -- an AppX install never elevating (msix-installer.md 4) is not a
+ reason to drop the daemon, only to change how its two elevated
+ operations get triggered (see the two tests below). --config still
+ means this file is read alone (app-builder-lib's getConfig), so it
+ cannot silently inherit Full's package.json build.nsis or any signing
+ config meant for NSIS.
+ """
+ assert MSIX_YML.exists(), f"{MSIX_YML} is missing"
+ yml = MSIX_YML.read_text(encoding="utf-8")
+
+ assert "appId: org.meshbay.client" in yml
+ assert "target: appx" in yml
+ assert "output: dist-msix" in yml
+
+ assert "node-runtime" in yml
+ assert "service.ps1" in yml
+ assert "service-mode.ps1" in yml
+ assert "firewall.ps1" in yml
+
+
+def test_msix_identity_matches_the_partner_center_reservation():
+ """
+ identityName/publisher/publisherDisplayName are assigned by Partner
+ Center's "App identity" page when the app name is reserved -- they
+ cannot be chosen freely, and a mismatch fails Store validation outright
+ rather than warning. Pins the exact reservation (done 2026-09-11) so a
+ future edit cannot silently drift back to electron-builder's own
+ unconfigured-build stand-in ("CN=ms", see windowsSignToolManager.js's
+ computePublisherName) without a test noticing.
+ publisherDisplayName is "MeshBay" as Partner Center assigned it, NOT
+ package.json's author company name ("MeshBay Team") -- AppXOptions.d.ts's
+ own default (company name from app metadata) would pick the wrong one if
+ this were ever left unset instead of explicit.
+ """
+ yml = MSIX_YML.read_text(encoding="utf-8")
+ assert "identityName: MeshBay.MeshBay" in yml
+ assert 'publisher: "CN=CE32BB0D-6B7C-4D3A-AA42-E259B778CAC9"' in yml
+ assert "publisherDisplayName: MeshBay" in yml
+ assert '"CN=ms"' not in yml, (
+ "must not have drifted back to electron-builder's own placeholder "
+ "publisher")
+
+
+def test_msix_declares_no_csc_on_purpose():
+ """
+ No certificateFile/certificateSubjectName/certificateSha1 anywhere in
+ this config -- per app-builder-lib's own windowsSignToolManager.js, an
+ AppX target built with no certificate configured is logged as "Windows
+ Store only build" and left unsigned; Microsoft signs it at publish time
+ (msix-installer.md 3). Configuring a cert here would be wasted work, not
+ extra safety, and would risk this target picking up whatever might one
+ day be configured for Full's NSIS signing if it were ever added to this
+ file instead of package.json's own build.win.
+ """
+ yml = MSIX_YML.read_text(encoding="utf-8")
+ for forbidden in ("certificateFile", "certificateSubjectName", "certificateSha1"):
+ assert forbidden not in yml
+
+
+def test_msix_declares_the_network_capabilities_firewall_ps1_would_add():
+ """
+ Matches firewall.ps1's own rules, which are `-Profile Any` (private AND
+ public network) -- msix-installer.md 8's #1 open item: whether Windows
+ actually auto-exempts a full-trust packaged app on the strength of
+ these declarations is unverified until sideloaded, but the declaration
+ itself must at least match what the elevated NSIS path grants today, or
+ an MSIX install would be silently narrower than Full/Light.
+ """
+ yml = MSIX_YML.read_text(encoding="utf-8")
+ caps_block = yml.split("capabilities:", 1)[1].split("customExtensionsPath", 1)[0]
+ assert "internetClientServer" in caps_block
+ assert "privateNetworkClientServer" in caps_block
+
+
+def test_msix_startup_task_targets_the_node_not_the_electron_shell():
+ """
+ addAutoLaunchExtension's built-in windows.startupTask (app-builder-lib's
+ AppxTarget.js) always targets the package's own main executable -- the
+ Electron shell -- which is not what "starts at sign in" means today
+ (main.js's WIN_STARTUP_VBS launches meshbay-node.exe directly, keeping
+ the daemon running whether or not the UI is ever opened). This config
+ must NOT use addAutoLaunchExtension for that reason, and must instead
+ supply its own extension via customExtensionsPath pointing at the node
+ binary's in-package path -- app\\resources\\node-runtime\\meshbay-node.exe,
+ derived from AppxTarget.js's own `"app\\\\" + appOutDir-relative path`
+ mapping (build() in that file), which is not the same prefix
+ process.resourcesPath resolves to at runtime and easy to get wrong.
+ """
+ yml = MSIX_YML.read_text(encoding="utf-8")
+ # The comment explaining *why* addAutoLaunchExtension is not used
+ # necessarily names it -- check the directive, not the prose (the same
+ # "parse directives, not text" mistake CLAUDE.md's engineering lessons
+ # already record for a differently-shaped bug).
+ lines = [ln.strip() for ln in yml.splitlines()]
+ assert not any(ln.startswith("addAutoLaunchExtension:") for ln in lines), (
+ "must not set addAutoLaunchExtension -- it always targets the "
+ "Electron shell, not the node binary (see this test's docstring)")
+ assert "customExtensionsPath: build/appx-extensions.xml" in yml
+
+ assert MSIX_EXTENSIONS_XML.exists(), f"{MSIX_EXTENSIONS_XML} is missing"
+ ext = MSIX_EXTENSIONS_XML.read_text(encoding="utf-8")
+ assert 'Category="windows.startupTask"' in ext
+ assert 'Executable="app\\resources\\node-runtime\\meshbay-node.exe"' in ext
+ assert 'EntryPoint="Windows.FullTrustApplication"' in ext
+
+
+def test_msix_ships_the_four_required_tile_images():
+ """
+ app-builder-lib's AppxTarget.js requires StoreLogo/Square44x44Logo/
+ Square150x150Logo/Wide310x150Logo regardless of showNameOnTiles, falling
+ back to its own vendor-bundled samples if build/appx/ does not supply
+ them. This repo points ELECTRON_BUILDER_WINDOWS_KITS_PATH at the system
+ Windows SDK instead of that vendor bundle (build-win-msix.ps1), which
+ has no samples to fall back to -- so a missing one here is a hard
+ makeappx failure, not a cosmetic gap. See build/appx/README.md for
+ where these came from.
+ """
+ appx_assets = CLIENT / "build" / "appx"
+ for name in ("StoreLogo.png", "Square44x44Logo.png",
+ "Square150x150Logo.png", "Wide310x150Logo.png"):
+ assert (appx_assets / name).exists(), f"{name} is missing from {appx_assets}"
+
+
+def test_build_win_msix_keeps_the_node_runtime_step():
+ """The opposite of Light's equivalent test: MSIX is Full's feature set,
+ repackaged -- build-node-runtime.ps1 must still run."""
+ assert BUILD_WIN_MSIX.exists(), f"{BUILD_WIN_MSIX} is missing"
+ src = BUILD_WIN_MSIX.read_text(encoding="utf-8")
+ assert "build-node-runtime.ps1" in src
+ assert "electron-builder.msix.yml" in src
+ assert "--config" in src
+ assert "--win appx" in src
+
+
+def test_dist_win_msix_delegates_to_the_msix_orchestrator():
+ pkg = _pkg()
+ scripts = pkg["scripts"]
+ assert "dist:win:msix" in scripts
+ assert "build-win-msix.ps1" in scripts["dist:win:msix"]
+
+
+def test_build_win_msix_points_electron_builder_at_the_system_sdk():
+ """
+ electron-builder's own bundled AppX tooling download (winCodeSign-*.7z)
+ extracts symlinks for binaries this target never uses and fails without
+ SeCreateSymbolicLinkPrivilege -- reproduced on this machine's build
+ shell. ELECTRON_BUILDER_WINDOWS_KITS_PATH (app-builder-lib's
+ getWindowsKitsBundle) is the documented escape hatch; this must be set
+ automatically, not left as a step a future build is expected to
+ remember.
+ """
+ src = BUILD_WIN_MSIX.read_text(encoding="utf-8")
+ assert "ELECTRON_BUILDER_WINDOWS_KITS_PATH" in src
+ assert "makeappx.exe" in src
+
+
+# ── main.js needs nothing new for this target ──────────────────────────────
+#
+# Unlike Light (which needed hasBundledNode/winCanElevateServiceMode/the
+# create-group gate because the bundle itself is smaller), MSIX ships
+# everything Full does, and the two elevation paths it cannot get from an
+# installer already exist independently of installer.nsh:
+# firewall.ps1's per-first-use Windows prompt (no admin needed for that
+# fallback -- see firewall.ps1's own header) and main.js's
+# winElevateServiceMode(), driven from the Node page ("the other door",
+# already exercised by test_can_elevate_checks_service_mode_ps1_actually_
+# exists above) rather than from setup. There is deliberately no
+# MSIX-specific test here pinning main.js: the Light-target tests above
+# already pin that winCanElevateServiceMode() checks for service-mode.ps1's
+# presence generically, which is exactly what makes it work for a third
+# packaged target without being told about it.
diff --git a/packaging/win/build-win-msix.ps1 b/packaging/win/build-win-msix.ps1
new file mode 100644
index 0000000..9f7f667
--- /dev/null
+++ b/packaging/win/build-win-msix.ps1
@@ -0,0 +1,134 @@
+<#
+.SYNOPSIS
+ Build the Windows "MSIX" package: same feature set as Full (bundled node
+ + ffmpeg), packaged for Microsoft Store submission instead of NSIS.
+
+.DESCRIPTION
+ See C:\Users\admin\devel\msix-installer.md for the plan this implements.
+ Unlike Light, this target does NOT drop anything from Full's feature
+ set -- it exists because Store certification of the NSIS "MSI/EXE"
+ submission failed for a reason MSIX sidesteps entirely (an unsigned,
+ internet-downloaded installer never gets a chance to run under
+ Microsoft's own unattended validation bot; see msix-installer.md §2),
+ not because the bundled node/service-mode/autostart machinery needed
+ removing. The one real change is *when* the two elevated operations
+ (firewall rule, service-mode install) can happen: an AppX/MSIX install
+ never elevates, so neither can run at install time the way
+ build/installer.nsh's customInstall macro does. Both already have an
+ elevation path that does not depend on the installer at all --
+ firewall.ps1's own per-first-use Windows prompt, and main.js's
+ winElevateServiceMode() ("the other door", already used by the Node
+ page today) -- so nothing in src/main.js needed to change for this
+ target to work.
+
+ Steps 1-5 are identical to build-win.ps1 (build-win-common.ps1 for 1-4,
+ build-node-runtime.ps1 for 5 -- this target ships the same frozen daemon
+ Full does). Step 6 hands electron-builder a standalone config
+ (electron-builder.msix.yml) via --config, same reasoning as Light: the
+ two targets' extraResources and signing configuration must never merge.
+
+ Output: packages/meshbay-client/dist-msix/MeshBay-<version>.appx
+
+.PARAMETER SkipFfmpeg
+ Passed through to build-node-runtime.ps1 -- skip bundling ffmpeg. Smaller,
+ streaming-less build for local iteration only.
+
+.PARAMETER NoElectronBump
+ Keep the pinned Electron instead of upgrading to the latest release.
+
+.PARAMETER SkipNodeRuntime
+ Reuse an existing packages/meshbay-client/node-runtime/ (faster iteration
+ on the electron-builder side).
+#>
+[CmdletBinding()]
+param(
+ [switch]$SkipFfmpeg,
+ [switch]$NoElectronBump,
+ [switch]$SkipNodeRuntime
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version Latest
+
+$WinDir = $PSScriptRoot
+$Repo = (Resolve-Path (Join-Path $WinDir "..\..")).Path
+$Client = Join-Path $Repo "packages\meshbay-client"
+$Config = Join-Path $WinDir "electron-builder.msix.yml"
+
+. (Join-Path $WinDir "build-win-common.ps1")
+
+if (-not (Test-Path $Config)) { throw "missing $Config" }
+
+# electron-builder's AppX target downloads its own bundled Windows Kits tools
+# (winCodeSign-*.7z) unless ELECTRON_BUILDER_WINDOWS_KITS_PATH already points
+# at a real one. That archive's 7z extraction creates symlinks for binaries
+# this target never uses (its macOS/Linux signing tools), and fails outright
+# without SeCreateSymbolicLinkPrivilege -- which this machine's build shell
+# does not hold. The Windows 10 SDK already installed here has everything
+# actually needed (makeappx.exe, signtool.exe); point electron-builder at
+# it instead of fighting that extraction. Only set if the caller hasn't
+# already pointed it somewhere themselves.
+if (-not $env:ELECTRON_BUILDER_WINDOWS_KITS_PATH) {
+ $kitsRoot = "C:\Program Files (x86)\Windows Kits\10\bin"
+ $kit = Get-ChildItem $kitsRoot -ErrorAction SilentlyContinue |
+ Where-Object { $_.PSIsContainer -and $_.Name -match '^\d+\.\d+\.\d+\.\d+$' `
+ -and (Test-Path (Join-Path $_.FullName "x64\makeappx.exe")) } |
+ Sort-Object Name -Descending | Select-Object -First 1
+ if (-not $kit) {
+ throw ("No Windows 10 SDK with makeappx.exe found under $kitsRoot -- " +
+ "install the Windows 10 SDK (the App Certification Kit / MSIX " +
+ "Packaging Tools component covers it), or set " +
+ "ELECTRON_BUILDER_WINDOWS_KITS_PATH yourself.")
+ }
+ $env:ELECTRON_BUILDER_WINDOWS_KITS_PATH = Join-Path $kit.FullName "x64"
+}
+Step "Windows Kits: $env:ELECTRON_BUILDER_WINDOWS_KITS_PATH"
+
+# --- 1. Node -------------------------------------------------------------
+Assert-NodeVersion
+
+Push-Location $Client
+try {
+ # --- 2. deps ------------------------------------------------------
+ Invoke-NpmCi
+
+ # --- 3. Electron: build against the latest release --------------
+ Invoke-ElectronBump -NoElectronBump:$NoElectronBump -WinDir $WinDir
+
+ # --- 4. UI -----------------------------------------------------
+ Invoke-SyncUi
+
+ # --- 5. node runtime ---------------------------------------
+ $rtExe = Join-Path $Client "node-runtime\meshbay-node.exe"
+ if ($SkipNodeRuntime -and (Test-Path $rtExe)) {
+ Step "reusing existing node-runtime/"
+ } else {
+ Step "building the bundled node (PyInstaller)"
+ $rtArgs = @{}
+ if ($SkipFfmpeg) { $rtArgs["SkipFfmpeg"] = $true }
+ & (Join-Path $WinDir "build-node-runtime.ps1") @rtArgs
+ if ($LASTEXITCODE -ne 0) { throw "build-node-runtime.ps1 failed" }
+ }
+
+ # --- 6. package ---------------------------------------
+ Step "electron-builder --win appx --config electron-builder.msix.yml"
+ & npx electron-builder --win appx --config $Config
+ if ($LASTEXITCODE -ne 0) { throw "electron-builder failed" }
+}
+finally {
+ Pop-Location
+}
+
+$pkg = Get-ChildItem (Join-Path $Client "dist-msix") -Filter "*.appx" -ErrorAction SilentlyContinue |
+ Sort-Object LastWriteTime | Select-Object -Last 1
+Write-Host ""
+if ($pkg) {
+ Write-Host "OK package: $($pkg.FullName)" -ForegroundColor Green
+ Write-Host (" ({0:N0} MB)" -f ($pkg.Length / 1MB))
+ Write-Host " unsigned by design -- Microsoft signs it at publish time" -ForegroundColor DarkGray
+ Write-Host " (msix-installer.md 3)." -ForegroundColor DarkGray
+}
+else {
+ Write-Host "!! no *.appx found in $Client\dist-msix" -ForegroundColor Red
+ exit 1
+}
diff --git a/packaging/win/electron-builder.msix.yml b/packaging/win/electron-builder.msix.yml
new file mode 100644
index 0000000..4026d46
--- /dev/null
+++ b/packaging/win/electron-builder.msix.yml
@@ -0,0 +1,113 @@
+# Standalone electron-builder config for the "MSIX" Windows target: same
+# feature set as Full (bundled node + ffmpeg), packaged for Microsoft Store
+# submission instead of NSIS. See C:\Users\admin\devel\msix-installer.md for
+# the plan this implements -- read that first, especially §4 (why the
+# installer can carry none of installer.nsh's elevation logic: an AppX/MSIX
+# install never elevates, by design) and §8 (what is still unverified here).
+#
+# Deliberately NOT layered onto package.json's `build` field, same reasoning
+# as electron-builder.light.yml: --config reads ONLY this file, so nothing
+# here can accidentally inherit or merge with Full's `nsis`/signing config.
+#
+# electron-builder's target key for this is `appx`, not `msix` -- this
+# version (app-builder-lib's AppxTarget.js, checked against the installed
+# ^26.15.3) has no separate `msix` target. It still produces a package
+# Partner Center's MSIX upload flow accepts; the mismatch is a naming
+# artifact of the tool, not a statement about which container format comes
+# out. Re-check this against whatever version is installed if it is ever
+# bumped -- do not assume the target name from memory.
+#
+# No CSC (code-signing cert) is configured for this target, and that is
+# correct, not an oversight: per app-builder-lib's own
+# windowsSignToolManager.js (computePublisherName), an AppX target built
+# with no certificate configured is logged as "Windows Store only build" and
+# left unsigned, with `publisher` written into the manifest as-is. Microsoft
+# signs the package itself at publish time (msix-installer.md §3) -- signing
+# it here first would be pointless work, not extra safety.
+
+appId: org.meshbay.client
+productName: MeshBay
+
+directories:
+ # Full's own build lives in dist/, Light's in dist-light/ -- a third,
+ # separate output dir so no two targets ever race on or clobber each
+ # other's files.
+ output: dist-msix
+
+files:
+ - src/**
+ - ui/**
+
+win:
+ target: appx
+ icon: build/icon.ico
+ artifactName: "MeshBay-${version}.${ext}"
+ extraResources:
+ # Same bundle as Full (package.json's own build.win.extraResources) --
+ # this target drops NSIS, not the node/ffmpeg runtime or the two service
+ # scripts. service-mode.ps1 in particular still works unmodified: it is
+ # invoked from main.js's winElevateServiceMode() on demand, from the
+ # running (unelevated) app, not from an installer step -- that path
+ # already existed before this target did (see "the other door" comment
+ # in src/main.js) and needs nothing new here beyond the file being
+ # present to find.
+ - from: node-runtime
+ to: node-runtime
+ filter:
+ - "**/*"
+ - from: ../../packaging/win/firewall.ps1
+ to: firewall.ps1
+ - from: ../../packaging/win/service.ps1
+ to: service.ps1
+ - from: ../../packaging/win/service-mode.ps1
+ to: service-mode.ps1
+
+appx:
+ # --- Real values, from Partner Center's "App identity" page (App
+ # management -> App identity), not chosen freely -- a mismatch here fails
+ # Store validation outright rather than warning. publisherDisplayName is
+ # "MeshBay" as Partner Center assigned it, NOT package.json's own author
+ # company name ("MeshBay Team") -- AppXOptions.d.ts's default (company
+ # name from app metadata) would silently pick the wrong one if this were
+ # left unset, so it must stay explicit even though it looks redundant
+ # next to `displayName`.
+ identityName: MeshBay.MeshBay
+ publisher: "CN=CE32BB0D-6B7C-4D3A-AA42-E259B778CAC9"
+ publisherDisplayName: MeshBay
+ applicationId: MeshBay
+ displayName: MeshBay
+ languages:
+ - en-US
+ - fr-FR
+ - es-ES
+ - pt-BR
+ - zh-CN
+ - ja-JP
+ - de-DE
+ - it-IT
+ - nl-NL
+ - pl-PL
+ # Both are the "common" (general, non-restricted) capability group per
+ # app-builder-lib's AppxCapabilities.js -- no special Store justification
+ # needed, unlike the `rescap`-namespaced restricted ones. Matches
+ # firewall.ps1's own rules, which are `-Profile Any` (private AND public
+ # network). Whether Windows Firewall actually auto-exempts a full-trust
+ # packaged app on the strength of these declarations -- eliminating the
+ # elevation firewall.ps1 exists for entirely -- is msix-installer.md §8's
+ # #1 open item: verify live before relying on it, the declaration alone
+ # only proves the manifest is well-formed.
+ capabilities:
+ - internetClientServer
+ - privateNetworkClientServer
+ # windows.startupTask, the MSIX-native equivalent of the NSIS "at sign in"
+ # mode's Startup-folder .vbs (main.js's WIN_STARTUP_VBS) -- but pointed at
+ # the node daemon, not the Electron shell, which is what `addAutoLaunchExtension:
+ # true` would do instead (it always targets the package's own main
+ # executable, per AppxTarget.js's `executable` macro -- there is no config
+ # switch to point it at a different bundled exe). build/appx-extensions.xml
+ # declares that extension by hand for exactly this reason. Whether
+ # Windows actually launches a *non-primary* bundled exe through this
+ # mechanism is the other open item in msix-installer.md §8 -- untested
+ # until sideloaded.
+ customExtensionsPath: build/appx-extensions.xml
+ showNameOnTiles: false