npm package: @hrefcl/apn
Description: Apple Push Notification service (APNs) for Node.js - Maintained fork with security updates and TypeScript support
Author: Href SpA
Latest version: 4.0.0

# @hrefcl/apn

Apple Push Notification service (APNs) for Node.js - Maintained fork with security updates and TypeScript support.

[![npm version](https://badge.fury.io/js/%40hrefcl%2Fapn.svg)](https://www.npmjs.com/package/@hrefcl/apn)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Why this fork?

The original `apn` package has been unmaintained since 2019 and contains **32 security vulnerabilities** in its dependencies. This fork:

- Updates all dependencies to secure versions
- Rewrites the codebase in TypeScript with full type definitions
- Uses native Node.js `http2` module (no deprecated packages)
- Maintains API compatibility where possible
- Adds support for modern APNs features (VoIP, Live Activities, MDM, Safari, etc.)

## Installation

```bash
npm install @hrefcl/apn
# or
yarn add @hrefcl/apn
```

## Requirements

- Node.js >= 16.0.0
- Apple Developer account with push notification capabilities
- APNs authentication key (.p8 file)

## Quick Start

```typescript
import { Provider, Notification } from '@hrefcl/apn';

// Create provider with token-based authentication
const provider = new Provider({
  token: {
    key: '/path/to/AuthKey_XXXXXXXXXX.p8',
    keyId: 'XXXXXXXXXX',
    teamId: 'YYYYYYYYYY'
  },
  production: false // Use true for production
});

// Create notification
const notification = new Notification({
  alert: {
    title: 'New Message',
    body: 'Hello from APNs!'
  },
  badge: 1,
  sound: 'default',
  topic: 'com.example.myapp'
});

// Send to device(s)
const result = await provider.send(notification, [
  'device-token-1',
  'device-token-2'
]);

console.log(`Sent: ${result.sent.length}`);
console.log(`Failed: ${result.failed.length}`);

// Handle failures
for (const failure of result.failed) {
  console.log(`Device: ${failure.device}`);
  console.log(`Reason: ${failure.response?.reason}`);
}

// Shutdown when done
provider.shutdown();
```

## API Reference

### Provider

```typescript
const provider = new Provider(options);
```

#### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `token` | Object | required | Token authentication options |
| `token.key` | string/Buffer | required | Path to .p8 file or key content |
| `token.keyId` | string | required | Key ID from Apple Developer |
| `token.teamId` | string | required | Team ID from Apple Developer |
| `production` | boolean | `false` | Use production environment |
| `host` | string | auto | Custom APNs host |
| `port` | number | `443` | Connection port |
| `requestTimeout` | number | `5000` | Request timeout (ms) |

#### Methods

- `send(notification, recipients)` - Send notification to device(s)
- `shutdown()` - Close all connections

### Notification

```typescript
const notification = new Notification(options);
```

Supported `pushType` values: `alert`, `background`, `voip`, `complication`, `fileprovider`, `mdm`, `liveactivity`, `location`, `pushtotalk`.

#### Options

| Option | Type | Description |
|--------|------|-------------|
| `alert` | string/Object | Alert content |
| `badge` | number | Badge number |
| `sound` | string | Sound filename |
| `contentAvailable` | boolean | Background fetch flag |
| `mutableContent` | boolean | Service extension flag |
| `category` | string | Action category |
| `threadId` | string | Thread identifier |
| `targetContentId` | string | Target content identifier |
| `interruptionLevel` | string | Focus interruption level |
| `relevanceScore` | number | Relevance score (0.0 to 1.0) |
| `filterCriteria` | string | Notification filter criteria |
| `staleDate` | number | Live Activity stale date |
| `contentState` | Object | Live Activity content state |
| `timestamp` | number | Live Activity timestamp |
| `event` | string | Live Activity event (start/update/end) |
| `dismissalDate` | number | Live Activity dismissal date |
| `topic` | string | Bundle ID |
| `expiry` | number/Date | Expiration time |
| `priority` | number | Priority (1-10) |
| `collapseId` | string | Collapse identifier |
| `pushType` | string | Push type |
| `payload` | Object | Custom data |
| `id` | string | apns-id header (UUID) |
| `urlArgs` | string[] | Safari push URL arguments |
| `mdm` | string | MDM magic string |

### Send Result

```typescript
interface SendResult {
  sent: Array<{ device: string }>;
  failed: Array<{
    device: string;
    status?: string;
    response?: { reason: string };
    error?: Error;
  }>;
}
```

## Error Handling

```typescript
const result = await provider.send(notification, tokens);

for (const failure of result.failed) {
  switch (failure.response?.reason) {
    case 'BadDeviceToken':
      // Invalid token format
      break;
    case 'Unregistered':
      // Token no longer valid - remove from database
      await removeToken(failure.device);
      break;
    case 'PayloadTooLarge':
      // Payload exceeds 4KB
      break;
    case 'TooManyRequests':
      // Rate limited - implement backoff
      break;
    default:
      console.error(`Unknown error: ${failure.response?.reason}`);
  }
}
```

## Background Notifications

```typescript
const notification = new Notification({
  contentAvailable: true,
  topic: 'com.example.app',
  pushType: 'background',
  payload: {
    action: 'sync-data'
  }
});
```

## Rich Notifications

```typescript
const notification = new Notification({
  alert: {
    title: 'Photo from John',
    subtitle: 'Vacation 2024',
    body: 'Check out this amazing sunset!'
  },
  mutableContent: true, // Enable service extension
  category: 'PHOTO',
  payload: {
    imageUrl: 'https://example.com/photo.jpg'
  }
});
```

## VoIP Notifications (5KB payload)

```typescript
const notification = new Notification({
  pushType: 'voip',
  topic: 'com.example.app', // auto-appends .voip
  payload: {
    callerId: 'user-123',
    callerName: 'Jane Doe'
  }
});
```

## Live Activities

```typescript
const notification = new Notification({
  pushType: 'liveactivity',
  topic: 'com.example.app', // auto-appends .push-type.liveactivity
  contentState: { score: '2-1' },
  event: 'update',
  timestamp: Date.now()
});
```

## Safari Push (url-args)

```typescript
const notification = new Notification({
  urlArgs: ['inbox', 'message-123'],
  payload: { action: 'open' }
});
```

## MDM

```typescript
const notification = new Notification({
  mdm: 'a1b2c3d4e5f6'
});
```

## apns-id

```typescript
const notification = new Notification({
  id: '6c7f28b1-2a0d-4c72-b1c4-2d5f0d6b8a64',
  alert: 'Hello'
});
```

## Migration from `apn`

This package is largely API-compatible with the original `apn` package:

```typescript
// Before (apn)
const apn = require('apn');
const provider = new apn.Provider(options);

// After (@hrefcl/apn)
import { Provider } from '@hrefcl/apn';
const provider = new Provider(options);
```

### Breaking Changes

1. **Node.js 16+ required** - Older versions not supported
2. **Token authentication only** - Certificate auth removed
3. **No proxy support** - May be added in future versions

## Security

This package fixes all 32 security vulnerabilities present in the original `apn` package:

- CVE-2022-23529 (jsonwebtoken)
- CVE-2022-23540 (jsonwebtoken)
- CVE-2022-24772 (node-forge)
- CVE-2022-24771 (node-forge)
- CVE-2020-7720 (node-forge)
- And 27 more...

## License

MIT - See [LICENSE](LICENSE) for details.

## Credits

- Original authors: Andrew Naylor, Florian Reinhart
- Maintained by: [Href SpA](https://href.cl)

## Links

- [npm package](https://www.npmjs.com/package/@hrefcl/apn)
- [GitHub repository](https://github.com/hrefcl/node-apn)
- [Apple APNs Documentation](https://developer.apple.com/documentation/usernotifications)