Murasaki

Deep links and file associations

Register URL schemes and document types, then handle every open request in Node Main.

Murasaki can register custom URL schemes and document extensions for packaged macOS and Windows applications. Both a cold launch and a request delivered to an already-running app arrive at the same openRequested() Node Main hook.

Configure handlers

Declare the schemes and extensions your application owns in murasaki.config.ts:

murasaki.config.ts
import { defineConfig } from 'murasaki'

export default defineConfig({
  appId: 'com.example.violet',
  productName: 'Violet',

  protocols: [
    { scheme: 'violet', name: 'Violet Link' },
  ],

  fileAssociations: [
    {
      extensions: ['vnote'],
      name: 'Violet Note',
      description: 'A note created with Violet',
      role: 'editor',
      mimeType: 'application/x-violet-note',
    },
  ],
})

Use extensions without a leading dot. Schemes and extensions are normalized to lowercase. Murasaki rejects duplicate values and reserved browser/OS schemes (including blob, file, http, https, javascript, mailto, ms-settings, tel, and murasaki), and malformed schemes, extensions, or MIME types at build time. See the configuration reference for every field and default.

Handle open requests

Create a Node Main entry and implement openRequested(context, event). Branch on each target's kind, not on the platform or transport:

src/main.ts
import { defineMain } from 'murasaki/main'

export default defineMain({
  async ready({ paths }) {
    // Open databases and initialize services first.
    console.log('data directory:', paths.data)
  },

  async openRequested(_context, event) {
    for (const target of event.targets) {
      if (target.kind === 'url') {
        const url = new URL(target.url)
        if (url.hostname !== 'open') continue

        // Validate and authorize the route before acting on it.
        console.log('open link:', url.pathname)
      } else {
        // Validate ownership, size, and contents before reading the file.
        console.log('open document:', target.path)
      }
    }
  },
})

Murasaki waits for ready() before calling openRequested(). This lets you initialize databases and other long-lived resources once, then use the same handler for cold-start and running-app requests.

Event reference

type OpenTarget =
  | { kind: 'url'; url: string; scheme: string }
  | { kind: 'file'; path: string }

interface OpenRequestEvent {
  activation: 'cold-start' | 'second-instance' | 'os-event'
  transport: 'argv' | 'open-url' | 'open-file'
  targets: OpenTarget[]
  cwd?: string
}
FieldMeaning
activationWhether this request accompanied the first launch, was forwarded from a second process, or came from a native OS event.
transportNative delivery mechanism. It is useful for diagnostics; normal application logic should use target.kind.
targetsOnly URLs and files matching configured schemes/extensions. An event can contain more than one file.
cwdWorking directory for an argv-based launch. Absent for native OS events.

On a second executable launch, secondInstance() still receives the raw argv/cwd, while openRequested() receives the recognized URL/file targets. Do not perform the same open action in both hooks. Use secondInstance() for generic activation behavior and openRequested() for semantic link/file handling.

Packaging behavior

ArtifactRegistration and delivery
macOS .appmurasaki bundle writes URL and document metadata into the app's Info.plist. Launch Services delivers URL/file opens, including opens sent to an already-running app. The .dmg carries this .app; move the app to Applications before final testing.
Windows NSIS .exeThe installer writes per-user or per-machine protocol and file-association registration according to installer.windows.installMode. A launched URL/file is normalized from argv.
Windows MSIThe MSI writes per-machine protocol and file-association registration. A launched URL/file is normalized from argv.
Windows portable archiveNo registry entries are installed. protocols and fileAssociations do not make the archive an OS handler.
LinuxProtocol and MIME registration is not implemented yet. Do not advertise Linux deep-link/file-association support.
murasaki devDoes not install system handler metadata. Test the installed/package artifact for OS integration.

Windows registration makes the app an available handler; it does not silently override the user's current default application. Keep appId stable between releases so installer ownership and association identifiers remain stable.

Test packaged handlers

After installing or moving the packaged app to Applications, test both a cold launch and an already-running app.

macOS
open 'violet://open/note-42'
open -a Violet ./example.vnote
Windows PowerShell
Start-Process 'violet://open/note-42'
Start-Process '.\example.vnote'

Also test the real NSIS/MSI uninstall and upgrade paths on a clean machine. Launching the executable manually with an argument is useful for handler code, but it does not verify that the OS registration was installed correctly.

Security

Every URL, file path, and launch argument is untrusted input. Any local process can launch your executable with a forged URL or path; a custom scheme does not prove who sent it.

  • Allowlist expected URL hosts, paths, actions, and parameter shapes.
  • Do not put bearer tokens or long-lived credentials in a deep-link URL. For sign-in callbacks, use a one-time code plus state and PKCE.
  • Redact query strings, fragments, and sensitive paths from logs and crash reports.
  • Check file size, permissions, format, and content before parsing. An expected extension is not proof of a safe file.
  • Account for symlinks and files changing between validation and use.
  • Keep handling in Node Main. Send only validated, minimal data to the renderer.

Next

On this page