Repository navigation
Ring the iPhone through Noodle Push when Noodle is closed (part of #35) - #130
Merged
Merged
Conversation
PushWire moves into its own package, Push/Wire, so HubLink can depend on it with nothing but swift-crypto; depending on the Push package itself pulled Hummingbird and APNSwift into every app's package resolution. The server builds the same code (the Dockerfile copies Wire/), and its vectors are unchanged. HubLink gains the callPush request (a CallGrant the phone signed for this Hub's key plus a per-Hub call key), LinkStatus.ringsThroughPush and callPushExpiresAt, the encrypted caller (LinkCallPushCaller, AES-GCM with the call key, bound to the call ID, with the Hub's own end of the ring), the Hub-side grant check, and HubPairing helpers that sign registrations and grants with the key the device paired with that Hub.
HubLinkService rings, on each callRinging, the owner's devices that gave a grant and are not following the Hub, signing a PushRingRequest with the Hub key. Grants Push says can never ring again (unregistered, registrationExpired, grantExpired, or rejected as badSealedToken, badGrant or grantTooLong) are dropped, and the device sees that in its status; a Hub-wide rate limit pauses rings for a minute. Logs name the outcome only. Grants and call keys are kept on HubDevice; the access file is written 0600. Personal Hubs (This Mac as a Hub, #122) use the same path, so calls from bots on the Mac push too.
The release build registers for VoIP pushes at launch, registers its token with Noodle Push per Hub (signed with that pairing's key) and sends each Hub a 14-day grant and a call key kept in the keychain, renewing it under 7 days left, with back-off so a refusing Hub or stale status causes no storm. A push is reported to CallKit before the delegate returns; a push from a Hub the phone is not in, unreadable, or too late is reported and ended. A ringing pushed call follows its Hub for callStopped; Answer connects within 6 s or leaves a missed-call notification; Decline goes to the Hub. CallKit now has one provider for the app, shared by every Hub's calls through PhoneCallLine, so a call a push reported and the same call ringing in-app are one call.
noodle-theo
Bot
force-pushed
the
theo/push-calls-clients
branch
from
October 21, 2026 20:50
2d7304b to
09058f6
Compare
This was referenced Oct 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #35: PRs 2–4 of the design. A bot's call now rings the iPhone through Noodle Push (live at push.usenoodle.app, #124) when Noodle Mobile is closed or away from its Hub. While the app is open and connected, the in-app ring handles the call as before.
Commits
1. Carry call grants for Noodle Push over the Hub link (
Push/,Shared/HubLink)Push/Wire. Depending on thePushpackage itself made SwiftPM resolve Hummingbird, APNSwift and NIO for every app; now HubLink pulls in only swift-crypto. The server code is unchanged: the Dockerfile copiesWire/, the vectors still pass, andPushWireTestsstill runs withswift test --package-path Push. Merging touchesPush/**, so push.yml redeploys the same server.callPush(LinkCallPush)request: aCallGrantthe phone signed for this Hub's key, plus a 32-byte call key for this Hub only. A request with no grant stops the Hub ringing the phone.LinkStatus.ringsThroughPushis the capability flag (same pattern askeepsAnnotations).callPushExpiresAttells the device what grant the Hub holds; nil means it holds none, so the device registers again.LinkCallPushCalleris the encrypted caller sent in the ring's blob: bot ID, name, conversation and the Hub's own end time. It is AES-GCM with the call key, with the call ID as associated data, so a blob can't be reused for another call.HubPairinghelpers sign registrations and grants with that pairing's device key. Each Hub gets its own sealed token and call key, so Hubs still can't tell they share a phone.2. Ring devices away from the Hub through Noodle Push (
HubCore, Noodle and Hub changelogs)HubLinkServicehandles eachcallRinging(fromHubBots.ringCall). It rings every owner device that has a grant and no live subscription, using the same rule as unread-reply notifications.CallPushersigns aPushRingRequestwith the Hub key and posts it to/v1/ring. It waits 10 s and does not retry.unregistered,registrationExpiredorgrantExpired, or onrejectedwith reasonbadSealedToken,badGrantorgrantTooLong. Any other refusal is only logged.addressLimitorhubLimit) it sends no rings for 60 s.HubDevice.callPush. The field is new and optional, and older files read it as none, so no migration or milestone is needed.access.jsonis now written 0600.PersonalHubuses the same path, so calls from bots on the Mac (This Mac as a Hub, Ring the iPhone for calls from bots on this Mac #122) also reach a closed phone.3. Ring Noodle Mobile through PushKit when it is closed or away (
Mobile)PKPushRegistry(.voIP)starts at launch, but only in the build with bundle IDcom.pdparchitect.noodle.mobile, on phones with CallKit.AfterFirstUnlockThisDeviceOnly.mustReportmetadata is honoured.hubhash and checks it against the Hubs it has joined, read straight from their folders.callStopped. Answer usesstartCall(answering:)and gives up after 6 s with a local "Missed call from …" notification. Decline usesdeclineCall.CXProvider, shared through per-HubPhoneCallLines. A call reported from a push and the same call ringing in the app are therefore one call.voipwas already inUIBackgroundModesandaps-environmentwas already set, so no entitlement change was needed.Security notes
access.jsonis 0600.LinkIdentitykeys, both the device key and the Hub key, are files rather than Keychain items, so they travel with backups. This predates this work.Tests (local, Xcode 27.1)
swift test --package-path Shared/HubLink: all pass, including 5 newLinkCallPushTests:swift test --package-path Push: 33 pass (6 PushWire, 27 PushCore).CallPushTests, using a fake Push endpoint over real QUIC pairing:access.jsonis 0600.PersonalHubTestschecks This Mac reportsringsThroughPush. All 353 HubCoreTests pass, and so doesTests/ReleaseAutomation.ringAwayhook switched off, the three ringing tests failed. With it on, they pass.CallPushTests:scripts/mobile.sh teststops at signing because this account has no Apple developer account or certificate. The app and its tests do compile for iOS unsigned (xcodebuild build-for-testing CODE_SIGNING_ALLOWED=NO). CI test-mobile runs them.Device test (Petko's iPhone, needs a build with the release bundle ID)
production. A build installed from Xcode registerssandbox(read fromaps-environmentin the embedded profile), but only if it uses the release bundle ID.Limitations
…mobile.local) cannot be rung. Push's VoIP topic is fixed tocom.pdparchitect.noodle.mobile.voip, and clients may not choose topics. A Dev build therefore doesn't register for VoIP. To test sandbox, build Debug withMOBILE_APP_BUNDLE_ID=com.pdparchitect.noodle.mobile. Otherwise test with TestFlight in production.