Add direct-APNs sender for WidgetKit push subscriptions#337
Open
hariharanjagan wants to merge 7 commits into
Open
Add direct-APNs sender for WidgetKit push subscriptions#337hariharanjagan wants to merge 7 commits into
hariharanjagan wants to merge 7 commits into
Conversation
When Core reports a tracked entity change for a widget, it POSTs a
{ push_subscription, push_token, registration_info } payload to the relay. The
push_token is an iOS 26 WidgetKit widget push token — a raw APNs token that FCM
can't route (messaging.send targets FCM registration tokens), so this path can't
go through the messaging SDK.
Add widget-push.js, which sends a silent WidgetKit refresh straight to APNs over
HTTP/2 (apns-push-type: widgets, topic <bundle>.push-type.widgets,
{"aps":{"content-changed":true}}), authenticated with a .p8-signed provider JWT
(ES256). It tries the production host and falls back to sandbox on
BadDeviceToken so it works for both Xcode and store builds.
sendPushNotification now routes requests carrying push_subscription to this
handler; the APNs key, key id and team id are provided as Functions secrets
(Secret Manager). Everything else still goes through FCM unchanged.
Add tests covering the success response, headers/topic, sandbox fallback, and
the missing-token / missing-app-id / missing-credentials / rejection paths.
Add direct-APNs sender for WidgetKit push subscriptions
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a direct-to-APNs delivery path for iOS 26 WidgetKit “widget push” subscriptions, since these widget tokens are raw APNs device tokens and cannot be targeted through FCM/Firebase Admin’s messaging.send().
Changes:
- Added
functions/widget-push.jsto sign an APNs provider JWT and POST a silent WidgetKit refresh (apns-push-type: widgets) directly to APNs, with production→sandbox fallback onBadDeviceToken. - Updated
functions/index.jsto route requests containingpush_subscriptionto the new APNs sender, while keeping existing notifications on the FCM path. - Added Jest coverage for success, required headers/topic, sandbox fallback, and failure cases.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| functions/widget-push.js | Implements APNs HTTP/2 sender with provider-JWT auth and sandbox fallback for WidgetKit push tokens. |
| functions/test/widget-push.test.js | Adds unit tests for the new APNs widget push handler behavior and error paths. |
| functions/index.js | Routes widget subscription payloads to the APNs sender and configures secrets for that function. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #337 +/- ##
==========================================
+ Coverage 79.62% 81.03% +1.40%
==========================================
Files 10 11 +1
Lines 648 712 +64
Branches 212 221 +9
==========================================
+ Hits 516 577 +61
- Misses 113 116 +3
Partials 19 19 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The widget path short-circuited to sendWidgetPush before handleRequest's rate limiter, leaving an unbounded path an attacker (or a runaway caller) could use to hammer APNs — raising cost and risking APNs throttling and device battery drain. Route widget pushes through the same per-token rate limiter: record the attempt, return 429 when the token exceeds the daily max, and record success/error after the send (best-effort). Export the shared limiter from handlers.js and add tests for the rate-limited and outcome-recording paths.
Follow-up to the widget rate-limiting: widget pushes now use a separate limiter instance with their own daily max (MAX_WIDGET_PUSHES_PER_DAY, default 100) rather than sharing the notification cap. Apple's WidgetKit push budget is small, so a lower cap fits and keeps widget traffic from consuming a device's normal notification allowance. Widget tokens are a distinct key namespace, so their counters never collide with FCM-token counters.
Feature/widget push
…rors Addresses two Copilot review findings on the widget-push relay: - Reject non-hex push tokens and non-bundle-id app ids before they reach the APNs :path and apns-topic headers, avoiding malformed/injected requests. - Close the HTTP/2 client when the session errors so the socket isn't leaked until GC, matching the stream error handler.
Adds tests for the two remaining uncovered lines flagged by Codecov: the stream-level request error handler (client close + reject) and the 500 returned when the rate-limit check itself throws. widget-push.js is now at 100% line coverage.
This was referenced Jul 6, 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.
When Core reports a tracked-entity change for a widget subscription, it POSTs a
{ push_subscription, push_token, registration_info } payload to the relay. That
push_token is an iOS 26 WidgetKit widget push token — a raw APNs token that
FCM can't route (messaging.send targets FCM registration tokens, and Firebase
has no widget-token field like apns.liveActivityToken).
This adds widget-push.js, which sends the silent WidgetKit refresh straight to
APNs over HTTP/2, bypassing the messaging SDK for this one path:
apns-push-type: widgets, topic .push-type.widgets, body
{"aps":{"content-changed":true}}
authenticated with a .p8-signed provider JWT (ES256; the private key never
leaves the function — only the signature goes to Apple)
tries the production host and falls back to sandbox on BadDeviceToken, so it
works for both Xcode and store builds
sendPushNotification now routes requests carrying push_subscription to this
handler. Everything else still goes through FCM unchanged, and Core is untouched.
Why not FCM
The widget push token is an Apple-only token FCM has never seen and has no field
to target (the apns.liveActivityToken trick has no widget equivalent as of
firebase-admin 13.5.0). So the only way to deliver it is straight to APNs.
Deployment note
The APNs key is provided as Functions secrets (Secret Manager) — the same APNs
auth key already used for FCM, just a copy the function can read to sign the JWT:
firebase functions:secrets:set APNS_KEY_P8 # the .p8 contents
firebase functions:secrets:set APNS_KEY_ID # 10-char Key ID
firebase functions:secrets:set APNS_TEAM_ID # 10-char Team ID
Only the function's runtime service account and project admins can read them
(IAM-gated); never the repo or logs.
Testing
test/widget-push.test.js covers the success response, headers/topic, the
sandbox fallback, and the missing-token / missing-app-id / missing-credentials /
rejection paths.
Full suite: 158 passing. Prettier + ESLint clean.
Verified end-to-end against real APNs on an iOS 26.5 device — the widget
reloaded with fresh data.
Context
The iOS and Core sides of this feature are already implemented; this is the relay
piece discussed in home-assistant/iOS#4782.
Related Pull Requests
Core - home-assistant/core#174943
iOS - home-assistant/iOS#4939