feat(push): real APNs delivery (token auth), inert without a key#129
Closed
oratis wants to merge 2 commits into
Closed
feat(push): real APNs delivery (token auth), inert without a key#129oratis wants to merge 2 commits into
oratis wants to merge 2 commits into
Conversation
Implements the APNs path end-to-end so it's ready the moment an Apple push key is provided — no more "would notify" stub when configured: - apnsConfigFromEnv() reads LISA_APNS_KEY_ID/_TEAM_ID/_KEY (.p8 PEM or path) /_TOPIC/_ENV; returns null (→ stub log) when unset. - buildApnsJwt() signs the ES256 provider JWT (raw IEEE-P1363, cached ~50min). - buildApnsPayload() builds aps.alert + a `link` custom key (the deep-link twin of the ntfy Click). - sendApns() POSTs /3/device/<token> over HTTP/2 with the apns-* headers; the poster is injectable for tests. PushBridge now delivers via APNs when configured, else logs the stub. Verified: npm run typecheck && npm run build && npm test -> 817 pass / 1 skip, incl. new tests that sign+verify the ES256 JWT with a throwaway P-256 key and assert the request shaping. Live delivery still needs a real key + device token (the external dependency). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- realApnsPost: settle exactly once (a `settled` guard kills the
client.on("error")/req.on("error") double-resolve race), always close the
HTTP/2 client, and add a 10s request timeout so a hung connection can't leak a
never-settled promise.
- sendApns JWT cache is now keyed by config identity (keyId/teamId), not just
time — a rotated/second key can't reuse a stale token (wrong kid/iss → 403).
- Add apns-expiration (high → "0" deliver-now-or-drop; default → 1h) so a
transient operational alert isn't stored and delivered stale.
- agentDeepLink / buildPairUrl emit %20 for spaces instead of "+", which iOS
URLComponents reads literally — so a device label / value round-trips.
Verified: typecheck + build clean; npm test -> 818 pass / 1 skip (+ space-
encoding and apns-expiration assertions).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Implements real APNs delivery, so operational push works natively on iOS the moment an Apple push key is provided — no more "would notify" stub. Stacked on #127 (both touch
src/web/push.ts).What
apnsConfigFromEnv()— readsLISA_APNS_KEY_ID/_TEAM_ID/_KEY(.p8 PEM contents or a path) /_TOPIC(defaultai.meetlisa.pocket) /_ENV(production→ prod host). Returns null → stub log when unset, so nothing changes for ntfy-only users.buildApnsJwt()— signs the ES256 provider JWT (raw IEEE-P1363 encoding, cached ~50 min per Apple's regen limit).buildApnsPayload()—aps.alert+ alinkcustom key (the deep-link twin of the ntfyClick, for tap-routing).sendApns()— POSTs/3/device/<token>over HTTP/2 with theapns-topic/apns-push-type/apns-priorityheaders; the poster is injectable for tests.PushBridgedelivers via APNs when configured, else logs the stub.Pairs with the iOS client-side registration (separate iOS PR) that captures the device token and registers it.
Verification
npm run typecheck && npm run build && npm test→ 817 pass / 1 skip. New tests sign and verify the ES256 JWT with a throwaway P-256 key (no real Apple key needed) and assert config gating, payload shape, and request shaping.Honest limit
Live delivery to Apple still needs a real APNs auth key + a real device token — that's the external dependency this can't carry. Everything up to the network call is implemented and tested; ntfy remains the zero-Apple-infra path.
🤖 Generated with Claude Code