Distribution
Shipping real native installers — cross-arch builds, signing, notarization, and CI.
murasaki bundle / murasaki installer ship real installers on macOS
(.app / .dmg) and Windows (a portable .zip, an NSIS .exe, and an
MSI .msi), cross-arch (arm64 / x64) on both. @murasakijs/native also ships
prebuilt binaries for Linux (x64/arm64); Linux app packaging is on the roadmap.
| Target | Bundle | Installer | Public code signing |
|---|---|---|---|
| macOS arm64 / x64 | .app + .app.zip | .dmg | Developer ID + notarization built in |
| Windows x64 / arm64 | portable folder + .zip | NSIS .exe; WiX .msi on Windows | Authenticode via SignTool built in |
| Linux | — | — | Planned |
A generated file is not automatically distribution-ready. Test the installed artifact on a clean machine, validate its signature, and verify every production dependency/resource. Murasaki is pre-1.0 and its Node dependency packaging does not cover every dynamic native addon or runtime-discovered asset layout yet.
Cross-arch builds
The bundle carries a portable, target-specific Node runtime (a checksum-verified
download from nodejs.org, cached under ~/.murasaki/node/) plus the compiled
native launcher binary — not whatever node happens to be running the CLI. That
means an Apple Silicon dev machine can also produce an Intel build, and vice
versa:
murasaki bundle --arch x64 # x64 .app on an Apple Silicon Mac
murasaki bundle --arch arm64 # arm64 .app on an Intel Mac
murasaki bundle --target win32-arm64 # an arm64 Windows bundlemurasaki installer forwards --arch / --target to bundle the same way.
Windows targets are win32-x64 / win32-arm64.
URL schemes and file associations
Declare packaged-app handlers in murasaki.config.ts:
import { defineConfig } from 'murasaki'
export default defineConfig({
appId: 'com.example.notes',
productName: 'Notes',
protocols: [{ scheme: 'example-notes', name: 'Notes link' }],
fileAssociations: [{
extensions: ['enote'],
name: 'Notes document',
role: 'editor',
mimeType: 'application/x-example-note',
}],
})The packaging result differs by target:
| Artifact | Registration behavior |
|---|---|
macOS .app / .dmg | CFBundleURLTypes, CFBundleDocumentTypes, and exported document UTIs are written to the app's Info.plist before signing. The DMG contains that app unchanged. |
Windows NSIS .exe | Registers protocols, ProgIDs, Open With entries, and Default Apps capabilities per-user by default, or per-machine with installMode: 'perMachine'. |
Windows WiX .msi | Registers the same handlers per-machine. |
Windows portable folder / .zip | Does not modify the registry or register itself automatically. |
| Linux | Registration and application packaging are planned. |
Matching cold-start, second-instance, and macOS open events are delivered to
Node Main's openRequested() hook after ready(). The Windows portable build
can still receive a registered-looking URL or file when it is passed directly
on its command line, but installation-free artifacts intentionally do not
claim operating-system defaults.
Windows registration makes the app an available handler; it does not overwrite the user's protected default-app choice. Test both a fresh install and an upgrade, and verify that uninstall removes only your app's handler entries.
Signing & notarization
Murasaki owns the signing orchestration, not the publisher identity. Supply
your Apple Developer ID or Windows certificate/Artifact Signing profile;
--sign deliberately fails instead of silently publishing unsigned output.
By default, murasaki bundle produces an ad-hoc signed .app, and
murasaki installer puts that app in a .dmg. This lets macOS verify local
bundle integrity, but it does not identify a trusted developer and the result
is not notarizable. For your own downloaded development artifact, you can
remove quarantine with:
xattr -dr com.apple.quarantine "<path>"Do not make that command the installation path for end users. Public, warning-free distribution requires Developer ID signing and notarization.
For warning-free distribution, sign and notarize with your own Apple Developer ID — Murasaki ships no certificate of its own:
murasaki bundle --sign # Developer ID-sign the .app
murasaki installer --sign --notarize # + submit the .dmg to Apple, staple the ticket--signsigns the.appwith a hardened runtime (Apple's documented flow: inner code first, then the outer bundle). The signing identity resolves from$MURASAKI_SIGN_IDENTITY, thenconfig.sign.identity, then the first "Developer ID Application" identity in your keychain. Entitlements come fromconfig.sign.entitlementsif set, otherwise a default plist granting Node the JIT / unsigned-executable-memory / no-library-validation entitlements it needs under the hardened runtime.--notarizerequires--sign(notarization only accepts Developer ID-signed code) and reads credentials fromAPPLE_ID,APPLE_TEAM_ID, andAPPLE_APP_PASSWORD(an app-specific password) — never from config or a file. It submits the.dmgto Apple's notary service, waits for the result, then staples the ticket so Gatekeeper can verify it offline.
Both require a paid Apple Developer Program membership.
Validate the macOS artifact
Run these against the final .app after signing and the final .dmg after
notarization:
codesign --verify --strict --verbose=2 "dist/bundle/My App.app"
codesign -dvvv --entitlements :- "dist/bundle/My App.app"
spctl -a -t open --context context:primary-signature -vv "dist/My App-1.0.0.dmg"
xcrun stapler validate "dist/My App-1.0.0.dmg"codesign --verify checks the app's code/resource seal. spctl evaluates the
distributed DMG against Gatekeeper policy. stapler validate checks that its
notarization ticket is attached. These answer different questions; run all of
them for a public release.
Windows installers and signing
murasaki bundle --target win32-x64 produces a portable folder and .zip.
murasaki installer additionally invokes tools already available on the build
host:
makensisproduces the NSIS.exeand may run on macOS or Windows.- WiX v4 produces the
.msiand runs on Windows.
The NSIS installer can be perUser (the default, no elevation) or
perMachine; MSI is always per-machine. See
Configuration for branding and
upgrade identity.
Run the signed release on Windows (cross-building unsigned Windows artifacts still works elsewhere):
pnpm exec murasaki bundle --target win32-x64 --sign
pnpm exec murasaki installer --target win32-x64 --signThe bundle command signs <productName>.exe before creating the portable ZIP.
The installer command reuses that signed payload, then signs the generated NSIS
setup and MSI. Every signing operation uses SHA-256, an RFC 3161 timestamp by
default, and a separate signtool verify /pa /v /tw pass. A signing or
verification failure stops the release.
Choose one signer. Config values can be overridden in CI without editing the checkout:
| Signer | Config | Environment override |
|---|---|---|
| PFX/P12 | sign.windows.certificateFile | MURASAKI_WINDOWS_CERTIFICATE_FILE + optional MURASAKI_WINDOWS_CERTIFICATE_PASSWORD |
| Imported certificate by subject | sign.windows.certificateSubjectName | MURASAKI_WINDOWS_CERTIFICATE_SUBJECT |
| Imported certificate by thumbprint | sign.windows.certificateSha1 | MURASAKI_WINDOWS_CERTIFICATE_SHA1 |
| Microsoft Artifact Signing | sign.windows.artifactSigning.{dlib,metadata} | MURASAKI_WINDOWS_ARTIFACT_SIGNING_DLIB + MURASAKI_WINDOWS_ARTIFACT_SIGNING_METADATA |
With none of those selectors, SignTool /a chooses the best code-signing
certificate from CurrentUser/My. Set certificateStore: 'localMachine' for
the machine store. Override the SignTool executable with
MURASAKI_SIGNTOOL_PATH, and the timestamp service with
MURASAKI_WINDOWS_TIMESTAMP_URL (false disables it, which is not recommended
for public releases).
For Microsoft Artifact Signing, dlib points to
Azure.CodeSigning.Dlib.dll and metadata to the non-secret account/profile
JSON. Authentication stays in Azure CLI, workload identity, or managed
identity; do not put credentials in that JSON or Murasaki config. Murasaki uses
Microsoft's Artifact Signing timestamp authority by default for this provider.
Do not treat the updater's Ed25519 manifest signature as an Authenticode replacement: it protects Murasaki's update channel, not Windows SmartScreen's publisher identity. A correctly signed new publisher can still see SmartScreen prompts while reputation develops.
Windows signed releases with GitHub Actions
This minimal PFX example writes the certificate only into the runner temp directory. Prefer an imported store certificate or cloud/HSM provider when that matches your certificate policy:
jobs:
windows:
runs-on: windows-2025
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: pnpm install --frozen-lockfile
- name: Materialize signing certificate
shell: pwsh
env:
CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_PFX }}
run: |
[IO.File]::WriteAllBytes(
"$env:RUNNER_TEMP\release.pfx",
[Convert]::FromBase64String($env:CERTIFICATE_BASE64)
)
- name: Build signed installers
env:
MURASAKI_WINDOWS_CERTIFICATE_FILE: ${{ runner.temp }}\release.pfx
MURASAKI_WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: pnpm exec murasaki installer --target win32-x64 --signmacOS signed releases with GitHub Actions
Build + (optionally) sign + notarize a .dmg on tag push and attach it to a
GitHub Release. Add this as .github/workflows/release.yml in your app:
name: Release
on:
push:
tags: ['v*']
jobs:
release:
runs-on: macos-14
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: pnpm install
- name: Import signing certificate
if: ${{ secrets.APPLE_CERTIFICATE_P12 != '' }}
env:
CERT_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
CERT_PW: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
KC="$RUNNER_TEMP/app.keychain-db"
security create-keychain -p "" "$KC"
security set-keychain-settings -lut 21600 "$KC"
security unlock-keychain -p "" "$KC"
echo "$CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$KC" -P "$CERT_PW" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "" "$KC"
security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
- name: Build installer
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
HAS_CERT: ${{ secrets.APPLE_CERTIFICATE_P12 != '' }}
run: |
if [ "$HAS_CERT" = "true" ]; then
pnpm exec murasaki installer --sign --notarize
else
pnpm exec murasaki installer
fi
- uses: softprops/action-gh-release@v2
with:
files: dist/*.dmgAdd these repository secrets to sign + notarize (omit them all for an unsigned
.dmg): APPLE_CERTIFICATE_P12 (base64 of your Developer ID .p12),
APPLE_CERTIFICATE_PASSWORD, APPLE_ID, APPLE_TEAM_ID, APPLE_APP_PASSWORD.
Never commit these secrets to config or source — --notarize deliberately
only reads them from the environment.
Release checklist
- Build from a clean checkout with a locked dependency graph.
- Launch the bundle before wrapping it in an installer.
- Install, update, and uninstall on clean target machines for every platform/architecture you publish.
- Test configured URL schemes and file associations for cold start and while the primary app instance is already running.
- Exercise Node Main shutdown, offline startup, and failed network requests.
- Verify macOS signatures/notarization and Windows Authenticode signatures.
- Generate and sign the Murasaki update manifest only after final payloads are immutable.
- Publish payloads,
latest.json, andlatest.json.sigtogether. - Keep signing credentials and
MURASAKI_UPDATE_KEYin CI secrets; never inmurasaki.config.ts.
Code-signing and update signing are independent. See Security and Auto-update before enabling in-app updates.