Files
nucleic/docs/PUSH_SETUP.md
T
abkslmandClaude Fable 5 1fe9d8898d mac: remove the direct APNS sender — the provider key must stay server-side
An app can't hold a secret the machine's owner can't extract (root +
debugger defeats bundling, Keychain, and obfuscation; the Secure
Enclave can't import external keys), and a leaked .p8 can push to
every user of the topic. So the push path is relay-only again: the
key lives exclusively in the relay's Worker secrets and the Mac only
asks the relay to send. Settings and PUSH_SETUP.md revert to the
relay-only form, with the rationale recorded in §4.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-02 16:30:57 -07:00

8.1 KiB
Raw Blame History

Manual setup — finishing iOS push, Live Activities & the relay wake path

Everything code-side is built, tested, and merged to dev (notification pipeline, actionable approvals, Live Activity extension, host PushRelayClient, worker /v1/push/register). What remains is credentialed, account-level, or requires a human at a device — this runbook is that list, in order. Design context: CLOUD_INFRA.md §23, UX_IOS.md §5.

0. Re-sign today's commits

1Password was locked during the working session, so SSH commit signing failed and these landed unsigned on claude/competent-pasteur-a5bb90 (and via merges on dev):

  • ac17282 ios parity, 0bd531e sync wire projections, babd047 push/Live Activity, plus the two dev merge commits and the docs commit that follows.

Unlock 1Password, then re-sign the branch (from the worktree or any checkout of it):

git rebase --exec 'git commit --amend --no-edit -S' d385571   # base of the branch

(or leave them unsigned if you don't enforce signing on dev). Push when satisfied: git push origin dev claude/competent-pasteur-a5bb90.

1. Apple Developer portal — APNS auth key

  1. developer.apple.com ▸ Certificates, IDs & Profiles ▸ Keys+ → name it (e.g. Nucleic APNS), check Apple Push Notifications service (APNs), Continue, Register.
  2. Download the .p8 once (it can't be re-downloaded) and note the Key ID.
  3. Team ID is L7UDTQ6F5W.

⚠️ Bundle-id gotcha: CLOUD_INFRA.md and the wrangler comments say the topic is xyz.blakeslee.nucleic.remote, but the app actually builds as xyz.blakeslee.nucleic-remote (dash, not dot — see PRODUCT_BUNDLE_IDENTIFIER). The APNS topic must be the real bundle id or every push is rejected with BadTopic.

2. Xcode — Push Notifications capability (one click)

The entitlements file (aps-environment = development) is already wired into the build (CODE_SIGN_ENTITLEMENTS), and NSSupportsLiveActivities is set. What Xcode must do once, because it registers the capability on the App ID server-side:

  1. Open ios/NucleicRemote/NucleicRemote.xcodeproj ▸ target NucleicRemoteSigning & Capabilities+ CapabilityPush Notifications.
  2. Let automatic signing regenerate the provisioning profile (or run any xcodebuild … -allowProvisioningUpdates build, e.g. scripts/ios-release.sh --archive-only).
  3. The NucleicRemoteWidgets extension target needs no push capability — Live Activities ride the app's NSSupportsLiveActivities.

3. Cloudflare — provision & deploy nucleic-edge

Prereq: the blakeslee.xyz zone on the account (the custom-domain routes need it). All commands from cloud/nucleic-edge/ with wrangler authed (npx wrangler login, or CLOUDFLARE_API_TOKEN).

  1. KV namespaceswrangler.jsonc carries ids; if they aren't real on this account, create and paste the returned ids over the existing ones:

    npx wrangler kv namespace create NUCLEIC_RELAY_TOKENS
    npx wrangler kv namespace create NUCLEIC_PUSH_TOKENS
    
  2. Secrets (npx wrangler secret put <NAME> each):

    Secret Value
    RELAY_TOKEN_SECRET fresh random, e.g. openssl rand -hex 32
    RELAY_ADMIN_SECRET fresh random — the host presents this as a bearer; you'll reuse it in §4
    APNS_KEY_P8 the full PEM contents of the .p8 from §1
    APNS_KEY_ID the Key ID from §1
    APNS_TEAM_ID L7UDTQ6F5W
    APNS_TOPIC xyz.blakeslee.nucleic-remote (see the gotcha in §1)
    APNS_ENV sandbox to start (Xcode-installed dev builds). TestFlight/App Store builds use production — the worker picks one host globally, so flip this when you move to TestFlight testing; mixed fleets aren't supported yet
  3. Deploy & verify:

    npm ci && npm test && npm run check
    npm run deploy
    curl https://api.nucleic.blakeslee.xyz/health        # → ok
    
  4. Smoke-test the push path (token from a real device, visible in §5):

    curl -i -XPOST https://api.nucleic.blakeslee.xyz/v1/push/register \
      -H "authorization: Bearer $RELAY_ADMIN_SECRET" -H 'content-type: application/json' \
      -d '{"deviceId":"iphone-XXXX","token":"<apns hex token>","env":"sandbox"}'
    curl -i -XPOST https://api.nucleic.blakeslee.xyz/v1/push/notify \
      -H "authorization: Bearer $RELAY_ADMIN_SECRET" -H 'content-type: application/json' \
      -d '{"deviceId":"iphone-XXXX"}'
    # → 200 and the phone shows "A session is waiting for your approval"
    

Optional: authorize the Cloudflare connectors in claude.ai connector settings if you want agent sessions to be able to run this provisioning next time.

4. Mac host — point it at the relay

These are runtime settings on the host Mac, not build settings. They're read when the sync server starts — process environment first (useful for make run / CI), then the same-named UserDefaults keys the Settings UI writes. A Finder-launched app never sees shell env vars, so the normal path is the in-app UI:

Nucleic ▸ Settings ▸ Remote ▸ "Approval push (relay)" — enter the relay URL (https://api.nucleic.blakeslee.xyz), the RELAY_ADMIN_SECRET from §3, pick the APNS environment (sandbox = Xcode dev installs, production = TestFlight / App Store), and hit Apply (it restarts remote access so the new config takes effect). Push stays entirely off while URL or secret is blank. Env-var form for scripted runs: NUCLEIC_RELAY_URL, NUCLEIC_RELAY_ADMIN_SECRET, NUCLEIC_APNS_ENV.

Why relay-only (no "send from this Mac" mode): the APNS provider key must never live on a user's machine — an app can't hold a secret the machine's owner can't extract (root + a debugger defeats bundling, the Keychain, and obfuscation alike; the Secure Enclave can't import external keys). A leaked .p8 lets anyone push to every user of the topic. So the key lives exclusively in the relay's Worker secrets, and the Mac only ever asks the relay to send. (Note the admin secret has the same client-side character — fine for your own Macs, but a distributed build should move to host-scoped relay tokens, per CLOUD_INFRA §2.1, before shipping relay config to anyone else.)

From then on: each phone's APNS token is mirrored to the relay after its sync Hello, and an approval arriving while a phone holds no live socket triggers a content-free wake (throttled to one per device per 30s).

5. On-device verification (needs a physical iPhone)

The simulator can't take this the last mile: the notifications permission dialog can't be accepted headlessly, and simulators don't get real APNS tokens. On a device (Xcode-run build):

  1. Launch → Allow the notifications prompt. Pair with the Mac.
  2. Local path (LAN): background the app, make an agent hit an approval on the Mac → banner with the session title + tool. Low-risk shows Allow/Deny on the banner (Allow asks for Face ID/passcode); destructive/network/host-exec shows no actions — tapping opens the in-app card with its Face ID gate. Answering on the Mac first withdraws the banner.
  3. Routing/badge: tapping a notification lands in that session; the app icon badge tracks the NEEDS YOU count.
  4. Live Activity: with a session running, the lock screen / Dynamic Island shows the aggregate "N running · M need you" activity; it updates as sessions change and disappears when everything is idle. (If nothing appears: iOS Settings ▸ NucleicRemote ▸ Live Activities.)
  5. Remote wake (relay): force-quit the app (or leave WiFi), trigger an approval on the Mac → the phone gets the localized approval.pending tickle via APNS; opening the app reconnects and shows the approval.

6. TestFlight

scripts/ios-release.sh now stamps the build channel (NUCLEIC_CHANNEL, default beta → the blue in-app banner; canary etc. work too). Remember from §3: TestFlight builds sign as aps-environment = production, so set the worker's APNS_ENV secret and the host's NUCLEIC_APNS_ENV to production when testing via TestFlight.