Murasaki

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.

TargetBundleInstallerPublic code signing
macOS arm64 / x64.app + .app.zip.dmgDeveloper ID + notarization built in
Windows x64 / arm64portable folder + .zipNSIS .exe; WiX .msi on WindowsAuthenticode via SignTool built in
LinuxPlanned

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 bundle

murasaki 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:

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:

ArtifactRegistration behavior
macOS .app / .dmgCFBundleURLTypes, CFBundleDocumentTypes, and exported document UTIs are written to the app's Info.plist before signing. The DMG contains that app unchanged.
Windows NSIS .exeRegisters protocols, ProgIDs, Open With entries, and Default Apps capabilities per-user by default, or per-machine with installMode: 'perMachine'.
Windows WiX .msiRegisters the same handlers per-machine.
Windows portable folder / .zipDoes not modify the registry or register itself automatically.
LinuxRegistration 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
  • --sign signs the .app with a hardened runtime (Apple's documented flow: inner code first, then the outer bundle). The signing identity resolves from $MURASAKI_SIGN_IDENTITY, then config.sign.identity, then the first "Developer ID Application" identity in your keychain. Entitlements come from config.sign.entitlements if set, otherwise a default plist granting Node the JIT / unsigned-executable-memory / no-library-validation entitlements it needs under the hardened runtime.
  • --notarize requires --sign (notarization only accepts Developer ID-signed code) and reads credentials from APPLE_ID, APPLE_TEAM_ID, and APPLE_APP_PASSWORD (an app-specific password) — never from config or a file. It submits the .dmg to 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:

  • makensis produces the NSIS .exe and may run on macOS or Windows.
  • WiX v4 produces the .msi and 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 --sign

The 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:

SignerConfigEnvironment override
PFX/P12sign.windows.certificateFileMURASAKI_WINDOWS_CERTIFICATE_FILE + optional MURASAKI_WINDOWS_CERTIFICATE_PASSWORD
Imported certificate by subjectsign.windows.certificateSubjectNameMURASAKI_WINDOWS_CERTIFICATE_SUBJECT
Imported certificate by thumbprintsign.windows.certificateSha1MURASAKI_WINDOWS_CERTIFICATE_SHA1
Microsoft Artifact Signingsign.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:

.github/workflows/release-windows.yml
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 --sign

macOS 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:

.github/workflows/release.yml
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/*.dmg

Add 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, and latest.json.sig together.
  • Keep signing credentials and MURASAKI_UPDATE_KEY in CI secrets; never in murasaki.config.ts.

Code-signing and update signing are independent. See Security and Auto-update before enabling in-app updates.

Next

Improve this page on GitHub

On this page