Skip to content

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

Closed
oratis wants to merge 2 commits into
feat/pairing-qr-renderfrom
feat/push-apns
Closed

feat(push): real APNs delivery (token auth), inert without a key#129
oratis wants to merge 2 commits into
feat/pairing-qr-renderfrom
feat/push-apns

Conversation

@oratis

@oratis oratis commented Jun 19, 2026

Copy link
Copy Markdown
Owner

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() — reads LISA_APNS_KEY_ID / _TEAM_ID / _KEY (.p8 PEM contents or a path) / _TOPIC (default ai.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 + a link custom key (the deep-link twin of the ntfy Click, for tap-routing).
  • sendApns() — POSTs /3/device/<token> over HTTP/2 with the apns-topic / apns-push-type / apns-priority headers; the poster is injectable for tests.
  • PushBridge delivers 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 test817 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

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 deleted the branch feat/pairing-qr-render June 19, 2026 10:52
@oratis oratis closed this Jun 19, 2026
@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