Apple Notarization Setup for ac-electron #
Date: 2026-04-17 Host: AestheticComputers-MacBook-Neo.local (not in machines.json)
Summary #
Wired up Apple notarization so auto-updates from releases.aesthetic.computer/desktop no longer trip Gatekeeper's "malware could not be verified" warning. Prior releases (including v0.1.38) were signed but not notarized, so downloaded DMGs refused to launch after being flagged with com.apple.quarantine. Built, notarized, and published v0.1.39 and confirmed it auto-installs cleanly over v0.1.38 with no Gatekeeper prompts.
Changes #
Credentials (vault) #
Added to aesthetic-computer-vault/ac-electron/.env, then re-encrypted to .env.gpg and shredded the plaintext:
APPLE_ID=me@jas.lifeAPPLE_APP_SPECIFIC_PASSWORD— generated at appleid.apple.com (Sign-In and Security → App-Specific Passwords), labelledac-electron-notaryAPPLE_TEAM_ID=FB5948YR3S
Also fixed a stale absolute path: CSC_LINK was pointing to /Users/jas/Desktop/code/aesthetic-computer/... (the jeffrey-macbook host's layout in machines.json), which doesn't exist on this machine. Switched to a relative path ../aesthetic-computer-vault/ac-electron/mac-developer-id.p12 so the same .env works on any host with the standard repo layout.
Code #
- ac-electron/package.json — bumped version to
0.1.39, flippedmac.notarizefromfalsetotrue, removed theafterSign: scripts/notarize.jshook (electron-builder's built-in notarize handles the.app), and added a newafterAllArtifactBuild: scripts/notarize-dmg.jshook (see DMG gotcha below). - ac-electron/scripts/notarize-dmg.js — new. After all artifacts are built, this signs each DMG with the Developer ID identity, submits it to
notarytool --wait, and staples the ticket. - ac-electron/scripts/notarize.js — kept as a documented fallback. Renamed
APPLE_NOTARIZE_PWD→APPLE_APP_SPECIFIC_PASSWORD(electron-builder's convention; already matches.env.template) and removed theCI/FORCE_NOTARIZEguard so credential presence is the sole gate. - ac-electron/CODE-SIGNING.md — renamed
APPLE_NOTARIZE_PWD→APPLE_APP_SPECIFIC_PASSWORDin docs, removed the now-unnecessaryFORCE_NOTARIZE=1export from the local-testing example.
Build + Notarization #
Command: rm -rf dist/ && npm run build:mac with the vault .env sourced beforehand.
- Clean build +
.appnotarization: ~5 min (electron-builder v25.1.8, electron 39.2.7) - The single log line
notarization successfulfrom electron-builder covers only the.app. The DMG gets built after that step and is never submitted. electron-builder v25 does not sign or notarize DMGs automatically — see Gotchas.
spctl Verification #
After the DMG was signed, notarized, and stapled (see Gotchas):
$ spctl -a -vvv -t install dist/Aesthetic.Computer-0.1.39-universal.dmg
dist/Aesthetic.Computer-0.1.39-universal.dmg: accepted
source=Notarized Developer ID
origin=Developer ID Application: Jeffrey Scudder (FB5948YR3S)
$ spctl -a -vvv -t exec dist/mac-universal/Aesthetic.Computer.app
dist/mac-universal/Aesthetic.Computer.app: accepted
source=Notarized Developer ID
origin=Developer ID Application: Jeffrey Scudder (FB5948YR3S)
Both .app and DMG pass the Gatekeeper check as Notarized Developer ID. xcrun stapler validate succeeds on both.
Publish #
Used aws s3 cp against the DO Spaces endpoint with creds from aesthetic-computer-vault/silo/.env (SPACES_KEY / SPACES_SECRET) — the current publish-release.mjs has requestChecksumCalculation: "WHEN_REQUIRED" which should work, but the CLI path is a known-good fallback for the 200MB DMG multipart.
Uploaded to s3://releases-aesthetic-computer/desktop/ with --acl public-read:
Aesthetic.Computer-0.1.39-universal.dmg(199 MB,public, max-age=31536000)Aesthetic.Computer-0.1.39-universal.dmg.blockmapAesthetic.Computer-0.1.39-universal.zip(192 MB)Aesthetic.Computer-0.1.39-universal.zip.blockmaplatest-mac.yml(no-cache, no-store, must-revalidate)
Then registered with silo: POST https://silo.aesthetic.computer/api/desktop/register with X-Publish-Secret header → {"ok":true,"version":"0.1.39"}. Confirmed GET /desktop/latest now returns 0.1.39.
Auto-update Test #
Launched the installed v0.1.38 (at /Applications/Aesthetic.Computer.app) with --remote-debugging-port=9222. Timeline:
- ~5s after launch: electron-updater checked
latest-mac.yml, saw 0.1.39 available. - Downloaded
Aesthetic.Computer-0.1.39-universal.zip(~200 MB) to~/Library/Caches/aesthetic-computer-updater/pending/. - Squirrel.Mac spun up its local proxy server and handed the ZIP to ShipIt.
SIGTERMto the Electron process — ShipIt ran the bundle swap.- ShipIt log at
~/Library/Caches/computer.aesthetic.app.ShipIt/ShipIt_stderr.logshows:Installation completed successfully. /Applications/Aesthetic.Computer.app/Contents/Info.plistnow reports0.1.39.spctl -a -vvv -t exec /Applications/Aesthetic.Computer.appreportssource=Notarized Developer ID.- Relaunching v0.1.39: no Gatekeeper prompt, app starts normally.
Gotchas #
electron-builder v25 doesn't notarize the DMG wrapper #
mac.notarize: true submits the .app bundle but not the DMG itself. Symptom: after the first build, xcrun stapler validate <dmg> reports "does not have a ticket stapled" and spctl -t install <dmg> returns rejected — source=no usable signature.
Fix applied: added afterAllArtifactBuild: scripts/notarize-dmg.js. The script signs each DMG with the Developer ID cert (codesign --sign "Developer ID Application: …" --timestamp --force <dmg>), submits via xcrun notarytool submit … --wait, then staples. Future npm run build:mac runs handle this automatically.
For this 0.1.39 release the hook wasn't wired yet, so the fix was applied manually to the artifacts in dist/. Because the DMG was re-signed after the original manifest was written, the size and sha512 of the DMG entry in latest-mac.yml were stale — regenerated them with openssl dgst -sha512 -binary <dmg> | base64 and edited the YAML before upload. The ZIP-based auto-update path uses the top-level path: …zip entry which was unaffected.
ShipIt had a stale failed install from earlier #
Found this in ~/Library/Caches/computer.aesthetic.app.ShipIt/ShipIt_stderr.log from a 00:39 attempt:
ShipIt[4629:435820] Installation error: Error Domain=NSOSStatusErrorDomain Code=-67068 "(null)"
errSecAuthFailed — the earlier unsigned 0.1.39 attempt couldn't pass Gatekeeper's install-time check, so Squirrel.Mac refused the swap. That's the exact failure mode the notarization fixes. The 11:53 run (post-notarization) logged Installation completed successfully on Resuming installation attempt 2 — ShipIt picked up the queued update and completed cleanly.
Vault .env had a stale absolute CSC_LINK #
Pre-existing CSC_LINK=/Users/jas/Desktop/code/aesthetic-computer/... was the jeffrey-macbook host's repo layout. Broke on any other machine. Fixed by switching to a relative path (resolved from ac-electron/ where npm run build:mac runs).
ADMIN_SUB=auth0|… in silo/.env errors when sourced #
The pipe in an unquoted value trips zsh when you source the file — cosmetic command not found: <suffix> error, the var still gets set to just auth0. Didn't break the publish; not touching it here, but worth quoting values if the file gets touched again.