Skip to content

feat(push): real APNs delivery (token auth), inert without a key#131

Merged
oratis merged 2 commits into
mainfrom
feat/push-apns
Jun 19, 2026
Merged

feat(push): real APNs delivery (token auth), inert without a key#131
oratis merged 2 commits into
mainfrom
feat/push-apns

Conversation

@oratis

@oratis oratis commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Real APNs delivery for operational push (the iOS-native path), implemented end-to-end so it works the moment an Apple push key is provided. Recreated PR — the original #129 was auto-closed when its stacked base branch (#127) was deleted during the merge; #127 is already on main, so this carries only the APNs commits.

  • apnsConfigFromEnv (LISA_APNS_KEY_ID/_TEAM_ID/_KEY/_TOPIC/_ENV; null → stub), buildApnsJwt (ES256 provider JWT, IEEE-P1363, config-keyed ~50min cache), buildApnsPayload (aps.alert + link deep-link), sendApns (HTTP/2 /3/device/<token>, injectable poster, 10s timeout, single-settle, apns-expiration). Wired into PushBridge.
  • Pairs with the iOS APNs registration that merged via the iOS follow-ups PR.

Pre-merge review fixes included (realApnsPost leak/timeout/double-resolve, config-keyed JWT cache, apns-expiration, %20 query encoding).

Verification: npm run typecheck && npm run build && npm test → 818 pass / 1 skip; APNs tests sign+verify a real ES256 JWT with a throwaway P-256 key. Live delivery needs a real Apple key + device (the external dependency).

🤖 Generated with Claude Code

oratis and others added 2 commits June 19, 2026 17:05
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>
@oratis oratis merged commit 6122699 into main Jun 19, 2026
2 checks passed
@oratis oratis deleted the feat/push-apns branch June 19, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant