const fs = require('fs'); const path = require('path'); const jwt = require('jsonwebtoken'); const http2 = require('http2'); const TEAM_ID = 'ZZMLW32PHH'; const KEY_ID = '3VJ5X6V9Q8'; const BUNDLE_ID = 'com.shenyan.room'; const KEY_PATH = path.join(__dirname, 'AuthKey_3VJ5X6V9Q8.p8'); // cache JWT for ~50min (max 1hr) let cachedToken = null; let tokenExpiry = 0; function getJWT() { if (cachedToken && Date.now() < tokenExpiry) return cachedToken; const key = fs.readFileSync(KEY_PATH, 'utf-8'); const now = Math.floor(Date.now() / 1000); const payload = { iss: TEAM_ID, iat: now }; const headers = { alg: 'ES256', kid: KEY_ID }; cachedToken = jwt.sign(payload, key, { algorithm: 'ES256', header: headers, expiresIn: '50m' }); tokenExpiry = now + 3000; // 50 min return cachedToken; } function sendAPNs(deviceToken, title, body) { return new Promise((resolve, reject) => { const payload = JSON.stringify({ aps: { alert: { title: title || '沈晏', body: body || '' }, sound: 'default', badge: 1, }, }); const client = http2.connect('https://api.sandbox.push.apple.com:443'); client.on('error', err => reject(err)); const req = client.request({ ':method': 'POST', ':path': `/3/device/${deviceToken}`, 'authorization': `bearer ${getJWT()}`, 'apns-topic': BUNDLE_ID, 'apns-push-type': 'alert', 'apns-expiration': '0', }); req.on('response', (headers) => { const status = headers[':status']; let data = ''; req.on('data', c => data += c); req.on('end', () => { client.close(); if (status === 200) resolve({ ok: true, status }); else reject(new Error(`APNs ${status}: ${data}`)); }); }); req.on('error', err => { client.close(); reject(err); }); req.end(payload); }); } module.exports = { sendAPNs };