Skip to content
C.W.K.
Lesson 04 of 05 · published

Links and the Stream Deck — a URL That Can Only Name

~12 min · url-scheme, stream-deck, input-validation, info-plist

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"A URL accepts only a saved command ID, never shell code or an arbitrary app path." — Tinker README

Why a URL Scheme at All

Dad runs editing sessions with a Stream Deck on the desk. The obvious integration would be a Stream Deck plugin, which means another codebase, another SDK version to chase, and another process that needs to talk to Tinker. The boring integration needs none of that. Stream Deck has a built-in System › Website action that opens any URL, and macOS opens a URL whose scheme an installed app declares by handing it to that app. So Tinker declares the tinker scheme in its Info.plist, and a key on the deck becomes tinker://command/edit.toggle. Separate "prepare" and "finish" keys use edit.apply and edit.restore.

The Parser Is a Gate

A URL scheme is an input channel any web page, document or other app on the Mac can trigger. That makes the handler a security boundary, and Tinker's is deliberately narrow. The scheme must be tinker, the host must be command, there must be exactly one path component after the slash, and there must be no query string and no fragment. Anything else is refused with "Use tinker://command/ followed by a saved command ID." Then the ID is looked up among saved commands; an unknown ID is an error, not an invitation. There is no way to pass a bundle ID, a file path, arguments or code through the URL. The worst a hostile page can do is run a command Dad already saved — which is why the release command and toggles are safe to expose and why a capability that could do harm would never be bindable this way without its own protections.

Every Command Shows Its Own URL

Settings prints each saved command's URL beside its shortcut recorder, selectable for copying, and each preset shows its toggle URL under the app list. Setting up a deck key is: copy the line, paste it into a Website action. The URL is derived from the command ID, so it never drifts from what Tinker will accept.

Same Engine, Labelled Door

When the URL arrives, the app delegate's application(_:open:) passes it to the model, which validates it and calls performCommand — the path shortcuts take too — which ends in the same run() and engine as a menu click, with the trigger recorded as Link. The Activity history therefore shows that the request came from a link — useful when a deck key is mislabelled and Dad wonders why broadcast mode just turned on.

Code

The URL handler: shape first, then lookup·swift
func openURL(_ url: URL) {
  guard url.scheme == "tinker", url.host == "command",
        url.query == nil, url.fragment == nil,
        url.pathComponents.count == 2 else {      // ["/", "<id>"]
    error = "Use tinker://command/ followed by a saved command ID."
    return
  }
  Task { await performCommand(url.lastPathComponent, trigger: .link) }
}

// AppDelegate
func application(_ application: NSApplication, open urls: [URL]) {
  for url in urls { model?.openURL(url) }
}
Declaring the scheme while building the bundle's Info.plist·python
import plistlib

info = {
    "CFBundleIdentifier": "com.cwk.tinker",
    "CFBundleName": "Tinker",
    "LSUIElement": True,                 # menu-bar agent: no Dock icon
    "CFBundleURLTypes": [
        {"CFBundleURLName": "Tinker commands", "CFBundleURLSchemes": ["tinker"]},
    ],
}
with open("Info.plist", "wb") as f:
    plistlib.dump(info, f)
Trying a link by hand·bash
# Runs the saved command exactly as a Stream Deck key would.
open "tinker://command/edit.toggle"

# Refused: a query is not allowed, so nothing runs.
open "tinker://command/edit.toggle?apps=com.cwk.voyager"

External links

Exercise

A friend proposes extending the scheme to tinker://hide?bundle=com.example.app so a deck key can hide any app without a preset. Write down what that would let a hostile web page do, and propose a design that gives the friend the convenience without widening the URL grammar.
Hint
With a free-form bundle parameter any page could hide arbitrary apps — a nuisance at best, and a way to conceal something at worst. The design that keeps the grammar narrow is the one Tinker already has: make a preset (or a capability command) for that app, which gives it a saved ID, and put that ID's URL on the key.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.