Closes #53.
The receive path was genuinely finished — kAEGetURL AppleEvent handler, delivery through __craftDeliverDeepLink, a buffer for a URL that arrives before the page loads — and it could never fire, because macOS only dispatches a URL to an app whose bundle claims the scheme under CFBundleURLTypes, and macOSInfoPlist emitted no such key. urlSchemes was a config field nothing read: an app could declare ['myapp'], typecheck, package, and never receive a link.
Verified against LaunchServices, not against the plist text
Two bundles, identical but for the key, registered and then asked -[NSWorkspace URLForApplicationToOpenURL:] for craftlsprobe://pair?code=123:
baseline HANDLER: <none>
registered WITHOUT urlSchemes HANDLER: <none> ← today
registered WITH urlSchemes HANDLER: .../with.appBoth test bundles were unregistered and deleted afterwards; lsregister -dump | grep -c dev.craft.lsprobe → 0.
Getting that probe to mean anything took two corrections worth recording, because both are silent:
- A bundle under
/private/tmpis flaggedin-temp-dirand its URL claims are ignored outright. - A bundle whose
CFBundleExecutableis a shell script is flaggedlaunch-disabled, same result.
Either one produces exactly the same <none> as a missing key, which is a good illustration of why "the plist contains the string" is not evidence for this particular fix.
Decisions
One URL type, all schemes. They are alternative ways to reach the same app, not different kinds of document; splitting them across dicts would invent a distinction LaunchServices does not make. CFBundleURLName is the bundle id — the one identifier guaranteed unique to the app.
A malformed scheme is now a package-time error. RFC 3986 syntax, checked. That failure is otherwise invisible in precisely the way this whole issue is about: LaunchServices drops the claim, the app installs and runs fine, and links never arrive.
Case-insensitive dedup, since that is how LaunchServices matches — ['harness', 'Harness'] is one scheme declared twice, not two.
Nothing is emitted when no schemes are declared, so every app packaged before this key existed produces a byte-identical plist.
What I did not do
The issue also asks to "fail loudly if deepLink.onUrl is reachable but the bundle declares no schemes". I don't think that one is deliverable as stated: craft.deepLink.onUrl is a pure-JS event subscription that never reaches native, so nothing at package time or at runtime can see it. Detecting it properly needs the page to be able to ask the native side what is actually wired — which is #49.
Related, and still true after this PR: MacOSConfig.urlSchemes (types.ts:1723) remains unconsumed, because CraftAppConfig/MacOSConfig as a whole are exported types with no runtime path into packageApp. The field that now works is PackageConfig.macos.urlSchemes, which is what packageApp actually reads. Worth its own issue rather than being smuggled in here.
The macos_deep_link.zig doc comment saying craft "can't do that for them" has been updated, since it is no longer true.
bun test 476 pass, zig build clean, tsc --noEmit clean, pickier clean. Control-checked: deleting the urlSchemes: opts.urlSchemes wiring fails the integration test, and deleting the plist rendering fails three unit tests.