import { BaseConnector } from '../../base/base.connector'; import type { IPushOptions, IPushProvider, ISendMessageSuccessResponse, WithPassthrough, PushSendResult, IPushConnector } from '../../types'; import { ChannelTypeEnum } from '../../types'; import type { ApnsConfig } from './apns.config'; import type { ApnsPushSendInput } from './apns.types'; export declare class ApnsPushConnector extends BaseConnector implements IPushProvider, IPushConnector { private config; readonly id = "apns"; readonly channelType: ChannelTypeEnum.PUSH; /** * True when NO transport was injected — i.e. we fall back to the built-in * `globalThis.fetch` (undici), which speaks HTTP/1.1 only. APNs mandates * HTTP/2, so that combination can never succeed; we fail fast (below) rather * than emit an opaque transport error. A consumer-injected `fetch` may be * HTTP/2-capable, so we do NOT block that case. */ private readonly usesDefaultTransport; constructor(config: ApnsConfig, fetchImpl?: typeof fetch); /** * APNs requires HTTP/2. Node's built-in `fetch` (undici) is HTTP/1.1-only, so * if the consumer injected no `fetch` we cannot succeed — surface a clear, * typed error up front instead of a confusing network failure mid-request. */ private assertHttp2Transport; /** * Thinwrap-native send. Implements `IPushConnector`. * * Auth flow: * 1. Resolve JWT via `getJwtViaHookOrFresh()` — either from the * consumer's `tokenCache` hook (if supplied and unexpired) or by * signing a fresh ES256 JWT via `createApnsJwt`. * 2. POST to `https://api.push.apple.com/3/device/` (prod) or * `https://api.sandbox.push.apple.com/3/device/` (sandbox). * * HTTP/2: APNs requires HTTP/2. Node's built-in `fetch` (undici) is * HTTP/1.1-ONLY — it does NOT transparently negotiate HTTP/2 — so a consumer * that injects no `fetch` cannot reach APNs; `assertHttp2Transport()` fails * fast in that case. An HTTP/2-capable `fetch` must be provided via * `ApnsConfig.fetch` (BYO-fetch contract). * * Single-recipient enforcement: `input.to` is the device token. * * Error handling: vendor errors map to canonical `ConnectorError` via * `mapVendorError`. On vendor 403 + `InvalidProviderToken` the wrapper * does NOT evict the hook cache — eviction is the consumer's responsibility. */ send(input: ApnsPushSendInput): Promise; /** * Resolve the APNs ES256 JWT, going through the consumer's `tokenCache` * hook when configured: * * - No `tokenCache`: sign fresh on every call (the stateless default). * - `tokenCache.get()` returns `{ token, expiresAt }` with `Date.now() < expiresAt`: * reuse the cached token; do NOT call `createApnsJwt`; do NOT call `set`. * - `tokenCache.get()` returns null or a stale entry: sign fresh, then call * `set(key, jwt, Date.now() + 50 * 60 * 1000)`. * * The cache key is exactly `'apns:::'` — fully * deterministic per-config; multiple apps/keys/teams in one process do not * collide. Vendor-rejection-of-cached-token eviction is the consumer's * responsibility (the wrapper holds no state). */ private getJwtViaHookOrFresh; /** * Build the APNs JSON request body from the narrowed `ApnsPushSendInput`. * Field-mapping table: * * input.title/body/subtitle → aps.alert.{title,body,subtitle} * input.sound → aps.sound * input.badge → aps.badge * input.aps (rest) → merged into aps verbatim (kebab-case preserved) * input.data → merged at root (custom keys per Apple spec) */ private buildApnsBody; /** * Build the per-request APNs HTTP/2 headers. Authorization and apns-topic * are always present; apns-push-type is set here only when the caller chose * it, otherwise {@link finalizePushType} infers it from the merged body. * apns-priority / apns-expiration / apns-collapse-id are emitted only when * the consumer set them (plus the priority 5 a background push requires). */ private buildApnsHeaders; /** * Map APNs error responses to canonical `ConnectorError` with the 6-value * `ProviderCode` union. Parses `Retry-After`: * parsed seconds embedded in `providerMessage` text; raw header * value on `cause.retryAfter`. No structured `retryAfterSeconds` field per * Retry is consumer policy (no retryAfterSeconds field). * * APNs does not always emit `Retry-After` on 429/503 (Apple's documented * backpressure signal is HTTP/2 stream reset; `Retry-After` is best-effort). * When absent, `cause.retryAfter` is `null` and no parenthetical is appended. */ private mapVendorError; /** * Resolves the JWT through `config.tokenCache` exactly like `.send()`: APNs * rejects a provider token that changes more than once per 20 minutes, so * this surface must not mint its own. * * Unlike `.send()`, which is single-recipient, this dispatches to multiple targets * via `Promise.allSettled` to match the Novu `IPushProvider` contract. */ sendMessage(options: IPushOptions, bridgeProviderData?: WithPassthrough>): Promise; }