diff --git a/.babelrc b/.babelrc index 7be671b..1703508 100644 --- a/.babelrc +++ b/.babelrc @@ -4,7 +4,7 @@ "@babel/preset-env", { "targets": { - "node": "14" + "node": "18" } } ] diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5721943..79281bb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -2,7 +2,9 @@ ## Project Overview -This repository implements a Node.js module for sending push notifications across multiple platforms: Apple (APN), Google (GCM/FCM), Windows (WNS), Amazon (ADM), and Web-Push. The core logic is in `lib/` and `src/`, with each platform handled by a dedicated file (e.g., `sendAPN.js`, `sendFCM.js`). +This repository implements a Node.js module for sending push notifications across multiple platforms: Apple (APN), Google (FCM), Windows (WNS), Amazon (ADM), and Web-Push. The core logic is in `lib/` and `src/`, with each platform handled by a dedicated file (e.g., `sendAPN.js`, `sendFCM.js`). + +**Note:** Legacy GCM (Google Cloud Messaging) support has been removed. All Android push notifications now route exclusively through Firebase Cloud Messaging (FCM) using the Firebase Admin SDK. ## Architecture & Data Flow @@ -14,40 +16,187 @@ This repository implements a Node.js module for sending push notifications acros ## Developer Workflows -- **Install:** `npm install` -- **Test:** Run all tests with `npm test`. Tests are in `test/` and cover basic flows and platform-specific cases. +- **Install:** `npm install` (requires Node.js 18+) +- **Test:** Run all tests with `npm test`. Tests are in `test/` and cover basic flows and platform-specific cases (87 tests, all passing). - **Debug:** Use the callback or Promise error/result from `push.send`. Each result object includes method, success/failure counts, and error details per device. - **Build:** No build step required for basic usage. ES6 is used, but compatible with ES5 via Babel if needed. ## Conventions & Patterns -- **Platform-specific files:** Each push service has its own file for isolation and clarity. +- **Platform-specific files:** Each push service has its own file for isolation and clarity. Legacy GCM is no longer supported. - **Unified Data Model:** The `data` object for notifications is normalized across platforms. See `README.md` for all supported fields. - **Error Handling:** Errors are unified and returned in the result array from `push.send`. - **RegId Format:** Prefer object format for registration IDs (`{id, type}`), but string format is supported for legacy reasons. -- **Chunking:** Android tokens are chunked in batches of 1,000 automatically. -- **Constants:** Use constants from `constants.js` for platform types. +- **Android Routing:** All Android push notifications route through FCM using Firebase Admin SDK. +- **Chunking:** Android tokens are chunked in batches of 1,000 automatically by FCM. +- **Constants:** Use constants from `constants.js` for platform types. Available constants: `FCM_METHOD`, `APN_METHOD`, `WNS_METHOD`, `ADM_METHOD`, `WEB_METHOD`, `UNKNOWN_METHOD`. + +## Firebase Cloud Messaging (FCM) - Modern Implementation + +### Message Building (src/utils/tools.js) + +**buildAndroidMessage(data, options)** +- Converts unified notification data to Firebase Admin SDK AndroidMessage format +- Returns plain JavaScript object (no wrapper functions) +- Properties mapped to camelCase (Firebase SDK standard) +- Removes undefined values for clean API calls +- Converts TTL from seconds to milliseconds +- Supports all 20+ AndroidNotification properties + +**buildAndroidNotification(data)** +- Maps input `data` object to AndroidNotification interface +- Supported properties: + - Basic: `title`, `body`, `icon`, `color`, `sound`, `tag`, `imageUrl` + - Localization: `titleLocKey`, `titleLocArgs`, `bodyLocKey`, `bodyLocArgs` + - Android-specific: `channelId`, `notificationCount`, `ticker`, `sticky`, `visibility` + - Behavior: `clickAction`, `priority`, `localOnly`, `eventTimestamp` + - Accessibility: `ticker` + - Vibration: `vibrateTimingsMillis`, `defaultVibrateTimings` + - Sound: `defaultSound` + - LED: `lightSettings`, `defaultLightSettings` + - Proxy: `proxy` (notification-level, values: 'allow', 'deny', 'if_priority_lowered') + +### FCM Configuration (src/sendFCM.js) + +**Initialization Options:** +- `credential` or `serviceAccountKey` (required) - Firebase authentication +- `projectId` (optional) - Explicit Google Cloud project ID +- `databaseURL` (optional) - Realtime Database URL +- `storageBucket` (optional) - Cloud Storage bucket +- `serviceAccountId` (optional) - Service account email +- `databaseAuthVariableOverride` (optional) - Auth override for RTDB rules +- `httpAgent` (optional) - HTTP proxy agent for network requests +- `httpsAgent` (optional) - HTTPS proxy agent for network requests + +All optional properties are dynamically added to Firebase initialization if defined. + +### Proxy Support + +**Two levels of proxy configuration:** + +1. **Network-level (SDK initialization)** + - `settings.fcm.httpAgent` and `settings.fcm.httpsAgent` + - Controls how Firebase Admin SDK communicates with Google servers + - Uses proxy agent libraries (http-proxy-agent, https-proxy-agent) + - Applied at app initialization + +2. **Notification-level (Android device)** + - `data.proxy` property in notification message + - Controls how Android devices handle notifications in proxy scenarios + - Values: 'allow', 'deny', 'if_priority_lowered' + - Per-message configuration + +### Message Format + +Firebase Admin SDK expects: +```javascript +{ + tokens: [...], + android: { + collapseKey: string, + priority: 'high' | 'normal', + ttl: number (milliseconds), + restrictedPackageName: string, + directBootOk: boolean, + data: { [key: string]: string }, + notification: { ...AndroidNotification properties... }, + fcmOptions: { analyticsLabel: string } + }, + apns: { ...APNs payload... } +} +``` ## Integration Points - **External Libraries:** - - APN: `node-apn` - - FCM: `firebase-admin` - - GCM: `node-gcm` + - APN: `@parse/node-apn` + - FCM: `firebase-admin` (all Android push notifications) - ADM: `node-adm` - WNS: `wns` - Web-Push: `web-push` + - Proxy: `http-proxy-agent`, `https-proxy-agent` (user-supplied) + - Note: Legacy `node-gcm` library has been removed - **Credentials:** Place service account keys and certificates in appropriate locations (see `README.md` for examples). ## Key Files & Directories -- `lib/` and `src/`: Main implementation (mirrored structure) -- `test/`: Test cases and sample credentials -- `README.md`: Usage, configuration, and data model reference +- `lib/` and `src/`: Main implementation (mirrored structure, both CommonJS) +- `lib/sendFCM.js` / `src/sendFCM.js`: Firebase Admin SDK integration +- `lib/utils/tools.js` / `src/utils/tools.js`: Message building utilities +- `lib/utils/fcmMessage.js` / `src/utils/fcmMessage.js`: FCM message formatting +- `test/send/sendFCM.js`: FCM test suite (10 test cases covering message format, proxy support, and Firebase AppOptions) +- `test/`: Test cases (87 tests total, all passing) +- `README.md`: Complete usage guide, configuration examples, and property documentation +- `.github/copilot-instructions.md`: This file - AI agent guidance + +## Recent Changes - Legacy GCM Removal & Firebase Admin SDK Alignment + +### What Changed + +**Removed:** +- `buildGcmMessage()` function (wrapper pattern with toJson() method) +- `buildGcmNotification()` function +- Post-processing delete statements for undefined properties +- References to legacy `node-gcm` library + +**Added:** +- `buildAndroidMessage()` - Direct Firebase Admin SDK compatible builder +- `buildAndroidNotification()` - Proper Android notification interface +- 15+ new Android notification properties (ticker, sticky, visibility, LED settings, etc.) +- Support for all 9 Firebase Admin SDK AppOptions (projectId, databaseURL, storageBucket, etc.) +- Proxy support at both SDK and notification levels +- Comprehensive test coverage (10 FCM-specific tests) + +### Migration Pattern + +**Before (Legacy GCM):** +```javascript +const message = buildGcmMessage(data).toJson(); +// Result: wrapper object with toJson() method +``` + +**After (Firebase Admin SDK):** +```javascript +const message = buildAndroidMessage(data); +// Result: plain object directly compatible with firebase-admin +``` + +### Property Naming + +All properties now use **camelCase** (Firebase Admin SDK standard): +- `android_channel_id` → `channelId` +- `title_loc_key` → `titleLocKey` +- `body_loc_key` → `bodyLocKey` +- `body_loc_args` → `bodyLocArgs` +- etc. + +### Testing + +New test suite added with 7 test cases: +- `should accept projectId in settings` +- `should accept databaseURL in settings` +- `should accept storageBucket in settings` +- `should accept serviceAccountId in settings` +- `should accept databaseAuthVariableOverride in settings` +- `should accept multiple Firebase AppOptions together` +- Plus 3 existing proxy tests (httpAgent, httpsAgent, both) + +All 87 tests passing, zero regressions. ## Example Usage -See `README.md` for a full example of settings, registration ID formats, and sending notifications. +See `README.md` for complete examples including: +- FCM settings configuration with all AppOptions +- Notification data with all supported properties +- Network proxy agent setup +- Notification-level proxy configuration + +## Future Considerations + +- Monitor Firebase Admin SDK updates for new AndroidMessage properties +- Consider typed interfaces for better TypeScript support +- Track Android notification API changes and feature additions +- Evaluate performance of dynamic property filtering approach --- diff --git a/README.md b/README.md index fab6192..900e80a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ A node.js module for interfacing with Apple Push Notification, Google Cloud Mess - [Requirements](#requirements) - [Features](#features) - [Usage](#usage) -- [GCM](#gcm) - [FCM](#fcm) - [APN](#apn) - [WNS](#wns) @@ -45,21 +44,23 @@ Node version >= 14.x.x ### 1. Import and setup push module -Include the settings for each device type. You should only include the settings for the devices that you expect to have. I.e. if your app is only available for android or for ios, you should only include `gcm` or `apn` respectively. +Include the settings for each device type. You should only include the settings for the devices that you expect to have. I.e. if your app is only available for Android or for iOS, you should only include `fcm` or `apn` respectively. ```js import PushNotifications from 'node-pushnotifications'; const settings = { - gcm: { - id: 'your-GCM-id', - phonegap: false, // phonegap compatibility mode, see below (defaults to false) - ... - }, fcm: { appName: 'localFcmAppName', serviceAccountKey: require('../firebase-project-service-account-key.json'), // firebase service-account-file.json, - credential: null // 'firebase-admin' Credential interface + credential: null, // 'firebase-admin' Credential interface + // Optional Firebase Admin SDK AppOptions + projectId: 'your-project-id', // Explicitly set the Google Cloud project ID + databaseURL: 'https://your-database.firebaseio.com', // Realtime Database URL (optional) + storageBucket: 'your-bucket.appspot.com', // Cloud Storage bucket (optional) + serviceAccountId: 'your-email@your-project.iam.gserviceaccount.com', // Service account email (optional) + httpAgent: undefined, // HTTP Agent for proxy support (optional) + httpsAgent: undefined, // HTTPS Agent for proxy support (optional) }, apn: { token: { @@ -87,26 +88,23 @@ const settings = { publicKey: '< URL Safe Base64 Encoded Public Key >', privateKey: '< URL Safe Base64 Encoded Private Key >', }, - gcmAPIKey: 'gcmkey', TTL: 2419200, contentEncoding: 'aes128gcm', headers: {} }, - isAlwaysUseFCM: false, // true all messages will be sent through gcm/fcm api - isLegacyGCM: false // if true gcm messages will be sent through node-gcm (deprecated api), if false gcm messages will be sent through 'firebase-admin' lib + isAlwaysUseFCM: false, // true all messages will be sent through FCM API }; const push = new PushNotifications(settings); ``` -- GCM options: see [node-gcm](https://github.com/ToothlessGear/node-gcm#custom-gcm-request-options) -- FCM options: see [firebase-admin](https://firebase.google.com/docs/admin/setup) (read [FCM](#fcm) section below!) +- FCM options: see [firebase-admin](https://firebase.google.com/docs/admin/setup) (read [FCM](#fcm) section below!) - used for Android and fallback for other platforms - APN options: see [node-apn](https://github.com/node-apn/node-apn/blob/master/doc/provider.markdown) - ADM options: see [node-adm](https://github.com/umano/node-adm) - WNS options: see [wns](https://github.com/tjanczuk/wns) - Web-push options: see [web-push](https://github.com/web-push-libs/web-push) -* `isAlwaysUseFCM`: use node-gcm to send notifications to GCM (by default), iOS, ADM and WNS. +* `isAlwaysUseFCM`: when set to `true`, will send all notifications through FCM instead of platform-specific services _iOS:_ It is recommended to use [provider authentication tokens](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html). You need the .p8 certificate that you can obtain in your [account membership](https://cloud.githubusercontent.com/assets/8225312/20380437/599a767c-aca2-11e6-82bd-3cbfc2feee33.png). You should ask for an _Apple Push Notification Authentication Key (Sandbox & Production)_ or _Apple Push Notification service SSL (Sandbox & Production)_. However, you can also use certificates. See [node-apn](https://github.com/node-apn/node-apn/wiki/Preparing-Certificates) to see how to prepare cert.pem and key.pem. @@ -143,10 +141,10 @@ It can be of 2 types: } ``` -Where type can be one of: 'apn', 'gcm', 'adm', 'wns', 'webPush'. The types are available as constants: +Where type can be one of: 'apn', 'fcm', 'adm', 'wns', 'webPush'. The types are available as constants: ```js -import { WEB, WNS, ADM, GCM, APN } from 'node-pushnotifications'; +import { WEB, WNS, ADM, FCM, APN } from 'node-pushnotifications'; const regId = { id: 'INSERT_YOUR_DEVICE_ID', @@ -170,18 +168,19 @@ In case of webPush, `id` needs to be as defined below for `Web subscription`. #### String regId (not recommended) -It is not recommended, as Apple stays that the [reg id is of variable length](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622958-application), which makes difficult to identify if it is a APN regId or GCM regId. +It is not recommended, as the [reg id is of variable length](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622958-application), which makes it difficult to identify if it is an APN regId or FCM regId. - `regId.substring(0, 4) === 'http'`: 'wns' - `/^(amzn[0-9]*.adm)/i.test(regId)`: 'adm' - `(regId.length === 64 || regId.length === 160) && /^[a-fA-F0-9]+$/.test(regId)`: 'apn' -- `regId.length > 64`: 'gcm' +- `regId.length > 64`: 'fcm' - otherwise: 'unknown' (the notification will not be sent) **Android:** -- If you provide more than 1.000 registration tokens, they will automatically be splitted in 1.000 chunks (see [this issue in gcm repo](https://github.com/ToothlessGear/node-gcm/issues/42)) -- You are able to send to device groups or other custom recipients instead of using a list of device tokens (see [node-gcm docs](https://github.com/ToothlessGear/node-gcm#recipients)). Documentation can be found in the GCM section.. +- All Android notifications are sent through Firebase Cloud Messaging (FCM) +- If you provide more than 1.000 registration tokens, they will automatically be split into 1.000 chunks +- You are able to send to custom topics or conditions through FCM (see [firebase-admin docs](https://firebase.google.com/docs/cloud-messaging)) Example: @@ -196,7 +195,7 @@ Create a JSON object with a title and message and send the notification. ```js const data = { title: 'New push notification', // REQUIRED for Android - topic: 'topic', // REQUIRED for iOS (apn and gcm) + topic: 'topic', // REQUIRED for iOS (apn and fcm) /* The topic of the notification. When using token-based authentication, specify the bundle ID of the app. * When using certificate-based authentication, the topic is usually your app's bundle ID. * More details can be found under https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns @@ -205,48 +204,61 @@ const data = { custom: { sender: 'AppFeel', }, - priority: 'high', // gcm, apn. Supported values are 'high' or 'normal' (gcm). Will be translated to 10 and 5 for apn. Defaults to 'high' - collapseKey: '', // gcm for android, used as collapseId in apn - contentAvailable: true, // gcm, apn. node-apn will translate true to 1 as required by apn. - delayWhileIdle: true, // gcm for android - restrictedPackageName: '', // gcm for android - dryRun: false, // gcm for android - icon: '', // gcm for android - image: '', // gcm for android - style: '', // gcm for android - picture: '', // gcm for android - tag: '', // gcm for android - color: '', // gcm for android - clickAction: '', // gcm for android. In ios, category will be used if not supplied - locKey: '', // gcm, apn - titleLocKey: '', // gcm, apn - locArgs: undefined, // gcm, apn. Expected format: Stringified Array - titleLocArgs: undefined, // gcm, apn. Expected format: Stringified Array - retries: 1, // gcm, apn + priority: 'high', // fcm, apn. Supported values are 'high' or 'normal' (fcm). Will be translated to 10 and 5 for apn. Defaults to 'high' + collapseKey: '', // fcm for android, used as collapseId in apn + contentAvailable: true, // fcm, apn. node-apn will translate true to 1 as required by apn. + delayWhileIdle: true, // fcm for android + restrictedPackageName: '', // fcm for android + dryRun: false, // fcm for android + directBootOk: false, // fcm for android. Allows direct boot mode + icon: '', // fcm for android + image: '', // fcm for android + style: '', // fcm for android + picture: '', // fcm for android + tag: '', // fcm for android + color: '', // fcm for android + clickAction: '', // fcm for android. In ios, category will be used if not supplied + locKey: '', // fcm, apn + titleLocKey: '', // fcm, apn + locArgs: undefined, // fcm, apn. Expected format: Stringified Array + titleLocArgs: undefined, // fcm, apn. Expected format: Stringified Array + retries: 1, // fcm, apn encoding: '', // apn - badge: 2, // gcm for ios, apn - sound: 'ping.aiff', // gcm, apn - android_channel_id: '', // gcm - Android Channel ID + badge: 2, // fcm for ios, apn + sound: 'ping.aiff', // fcm, apn + android_channel_id: '', // fcm - Android Channel ID notificationCount: 0, // fcm for android. badge can be used for both fcm and apn + ticker: '', // fcm for android. Ticker text for accessibility + sticky: false, // fcm for android. Notification persists when clicked + visibility: 'public', // fcm for android. Can be 'public', 'private', or 'secret' + localOnly: false, // fcm for android. Local-only notification (Wear OS) + eventTimestamp: undefined, // fcm for android. Date object for event time + notificationPriority: 'default', // fcm for android. Can be 'min', 'low', 'default', 'high', 'max' + vibrateTimingsMillis: undefined, // fcm for android. Array of vibration durations in milliseconds + defaultVibrateTimings: false, // fcm for android. Use system default vibration + defaultSound: false, // fcm for android. Use system default sound + lightSettings: undefined, // fcm for android. LED light settings object + defaultLightSettings: false, // fcm for android. Use system default LED settings + analyticsLabel: '', // fcm for android. Analytics label for FCM alert: { // apn, will take precedence over title and body title: 'title', body: 'body' // details: https://github.com/node-apn/node-apn/blob/master/doc/notification.markdown#convenience-setters }, - silent: false, // gcm, apn, will override badge, sound, alert and priority if set to true on iOS, will omit `notification` property and send as data-only on Android/GCM + silent: false, // fcm, apn, will override badge, sound, alert and priority if set to true on iOS, will omit `notification` property and send as data-only on Android/FCM /* * A string is also accepted as a payload for alert * Your notification won't appear on ios if alert is empty object * If alert is an empty string the regular 'title' and 'body' will show in Notification */ // alert: '', - launchImage: '', // apn and gcm for ios - action: '', // apn and gcm for ios - category: '', // apn and gcm for ios - // mdm: '', // apn and gcm for ios. Use this to send Mobile Device Management commands. + launchImage: '', // apn and fcm for ios + action: '', // apn and fcm for ios + category: '', // apn and fcm for ios + // mdm: '', // apn and fcm for ios. Use this to send Mobile Device Management commands. // https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/MobileDeviceManagementProtocolRef/3-MDM_Protocol/MDM_Protocol.html - urlArgs: '', // apn and gcm for ios - truncateAtWordEnd: true, // apn and gcm for ios + urlArgs: '', // apn and fcm for ios + truncateAtWordEnd: true, // apn and fcm for ios mutableContent: 0, // apn threadId: '', // apn pushType: undefined, // apn. valid values are 'alert' and 'background' (https://github.com/parse-community/node-apn/blob/master/doc/notification.markdown#notificationpushtype) @@ -279,7 +291,7 @@ push.send(registrationIds, data) ```js [ { - method: 'gcm', // The method used send notifications and which this info is related to + method: 'fcm', // The method used send notifications and which this info is related to multicastId: [], // (only Android) Array with unique ID (number) identifying the multicast message, one identifier for each chunk of 1.000 notifications) success: 0, // Number of notifications that have been successfully sent. It does not mean that the notification has been deliveried. failure: 0, // Number of notifications that have been failed to be send. @@ -287,7 +299,7 @@ push.send(registrationIds, data) messageId: '', // (only for android) String specifying a unique ID for each successfully processed message or undefined if error regId: value, // The current registrationId (device token id). Beware: For Android this may change if Google invalidates the previous device token. Use "originalRegId" if you are interested in when this changed occurs. originalRegId: value, // (only for android) The registrationId that was sent to the push.send() method. Compare this with field "regId" in order to know when the original registrationId (device token id) gets changed. - error: new Error('unknown'), // If any, there will be an Error object here for depuration purposes (when possible it will come form source libraries aka apn, node-gcm) + error: new Error('unknown'), // If any, there will be an Error object here for debugging purposes errorMsg: 'some error', // If any, will include the error message from the respective provider module }], }, @@ -310,155 +322,70 @@ push.send(registrationIds, data) ] ``` -## GCM - -**NOTE:** If you provide more than 1.000 registration tokens, they will automatically be splitted in 1.000 chunks (see [this issue in gcm repo](https://github.com/ToothlessGear/node-gcm/issues/42)) - -The following parameters are used to create a GCM message. See https://developers.google.com/cloud-messaging/http-server-ref#table5 for more info: - -```js - // Set default custom data from data - let custom; - if (typeof data.custom === 'string') { - custom = { - message: data.custom, - }; - } else if (typeof data.custom === 'object') { - custom = Object.assign({}, data.custom); - } else { - custom = { - data: data.custom, - }; - } - - custom.title = custom.title || data.title; - custom.message = custom.message || data.body; - custom.sound = custom.sound || data.sound; - custom.icon = custom.icon || data.icon; - custom.msgcnt = custom.msgcnt || data.badge; - if (opts.phonegap === true && data.contentAvailable) { - custom['content-available'] = 1; - } - - const message = new gcm.Message({ // See https://developers.google.com/cloud-messaging/http-server-ref#table5 - collapseKey: data.collapseKey, - priority: data.priority === 'normal' ? data.priority : 'high', - contentAvailable: data.contentAvailable || false, - delayWhileIdle: data.delayWhileIdle || false, // Deprecated from Nov 15th 2016 (will be ignored) - timeToLive: data.expiry - Math.floor(Date.now() / 1000) || data.timeToLive || 28 * 86400, - restrictedPackageName: data.restrictedPackageName, - dryRun: data.dryRun || false, - data: data.custom, - notification: { - title: data.title, // Android, iOS (Watch) - body: data.body, // Android, iOS - icon: data.icon, // Android - image: data.image, // Android - style: data.style, // Android - picture: data.picture, // Android - sound: data.sound, // Android, iOS - badge: data.badge, // iOS - tag: data.tag, // Android - color: data.color, // Android - click_action: data.clickAction || data.category, // Android, iOS - body_loc_key: data.locKey, // Android, iOS - body_loc_args: data.locArgs, // Android, iOS - title_loc_key: data.titleLocKey, // Android, iOS - title_loc_args: data.titleLocArgs, // Android, iOS - android_channel_id: data.android_channel_id, // Android - }, - } -``` - -_data is the parameter in `push.send(registrationIds, data)`_ - -- [See node-gcm fields](https://github.com/ToothlessGear/node-gcm#usage) - -**Note:** parameters are duplicated in data and in notification, so in fact they are being send as: - -```js - data: { - title: 'title', - message: 'body', - sound: 'mySound.aiff', - icon: undefined, - msgcnt: undefined - // Any custom data - sender: 'appfeel-test', - }, - notification: { - title: 'title', - body: 'body', - icon: undefined, - image: undefined, - style: undefined, - picture: undefined, - sound: 'mySound.aiff', - badge: undefined, - tag: undefined, - color: undefined, - click_action: undefined, - body_loc_key: undefined, - body_loc_args: undefined, - title_loc_key: undefined, - title_loc_args: undefined, - android_channel_id: undefined - } -``` - -In that way, they can be accessed in android in the following two ways: - -```java - String title = extras.getString("title"); - title = title != null ? title : extras.getString("gcm.notification.title"); -``` - -### Silent push notifications - -GCM supports silent push notifications which are not displayed to the user but only used to transmit data. - -```js -const silentPushData = { - topic: 'yourTopic', - silent: true, - custom: { - yourKey: 'yourValue', - ... - } -} -``` - -Internally, `silent: true` will tell `node-gcm` _not_ to send the `notification` property and only send the `custom` property. If you don't specify `silent: true` then the push notifications will still be visible on the device. Note that this is nearly the same behavior as `phoneGap: true` and will set `content-available` to `true`. - -### Send to custom recipients (device groups or topics) - -In order to override the default behaviour of sending the notifications to a list of device tokens, -you can pass a `recipients` field with your desired recipients. Supported fields are `to` and `condition` as documented in the [node-gcm docs](https://github.com/ToothlessGear/node-gcm#recipients). - -Example: - -```javascript -const dataWithRecipientTo = { ...yourData, recipients: { to: 'topicName' } }; -const dataWithRecipientCondition = { ...yourData, recipients: { condition: 'topicName' } }; - -push.send(registrationIds, dataWithRecipientTo) - .then((results) => { ... }) - .catch((err) => { ... }); -``` - -Be aware that the presence of a valid `data.recipient` field will take precendence over any Android device tokens passed with the `registrationIds`. - -### PhoneGap compatibility mode +## FCM -In case your app is written with Cordova / Ionic and you are using the [PhoneGap PushPlugin](https://github.com/phonegap/phonegap-plugin-push/), -you can use the `phonegap` setting in order to adapt to the recommended behaviour described in -[https://github.com/phonegap/phonegap-plugin-push/blob/master/docs/PAYLOAD.md#android-behaviour](https://github.com/phonegap/phonegap-plugin-push/blob/master/docs/PAYLOAD.md#android-behaviour). +All Android push notifications are sent through Firebase Cloud Messaging (FCM) using the [firebase-admin](https://github.com/firebase/firebase-admin-node) library. + +The following parameters are used to create an FCM Android message (following the [Firebase Admin SDK AndroidConfig interface](https://firebase.google.com/docs/reference/admin/node/admin.messaging.AndroidConfig)): + +**AndroidConfig properties:** + +- `collapseKey` - Collapse key for message grouping +- `priority` - Message priority: 'high' (default) or 'normal' +- `ttl` - Time to live in milliseconds (converted from seconds) +- `restrictedPackageName` - Package name restriction +- `directBootOk` - Allow delivery in direct boot mode +- `data` - Custom data fields (key-value pairs) +- `notification` - Android notification properties (see below) +- `fcmOptions` - FCM options including `analyticsLabel` + +**AndroidNotification properties:** + +- `title` - Notification title +- `body` - Notification body +- `icon` - Notification icon resource +- `color` - Notification color (#rrggbb format) +- `sound` - Notification sound file +- `tag` - Notification tag for replacing existing notifications +- `imageUrl` - Image URL to display in notification +- `clickAction` - Action to launch when notification is clicked +- `bodyLocKey` / `bodyLocArgs` - Localized body text +- `titleLocKey` / `titleLocArgs` - Localized title text +- `channelId` - Android notification channel ID +- `notificationCount` - Number of unread notifications +- `ticker` - Ticker text for accessibility +- `sticky` - Notification persists when clicked +- `visibility` - Visibility level: 'public', 'private', or 'secret' +- `priority` - Notification priority: 'min', 'low', 'default', 'high', or 'max' +- `eventTimestamp` - Date object for event time +- `localOnly` - Local-only notification (for Wear OS) +- `vibrateTimingsMillis` - Vibration pattern (array of milliseconds) +- `defaultVibrateTimings` - Use system default vibration +- `defaultSound` - Use system default sound +- `lightSettings` - LED light configuration object +- `defaultLightSettings` - Use system default LED settings +- `proxy` - Proxy setting: 'allow', 'deny', or 'if_priority_lowered' + +Example usage: ```js -const settings = { - gcm: { - id: '', - phonegap: true, +const data = { + title: 'Title', + body: 'Body text', + icon: 'ic_notification', + color: '#FF0000', + sound: 'notification_sound', + clickAction: 'OPEN_ACTIVITY', + android_channel_id: 'default_channel', + tag: 'my_notification', + badge: 1, + notificationPriority: 'high', + ticker: 'New notification', + sticky: false, + visibility: 'public', + analyticsLabel: 'my_analytics_label', + custom: { + key: 'value', }, }; ``` @@ -533,14 +460,22 @@ const silentPushData = { ## FCM -The following parameters are used to create an FCM message (Android/APN): -[node-gcm](https://github.com/ToothlessGear/node-gcm) lib for `GCM` method use old firebase api (will be [deprecated ](https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=en&authuser=0)) -Settings: +All Android push notifications are sent through Firebase Cloud Messaging (FCM) using the [firebase-admin](https://github.com/firebase/firebase-admin-node) library. + +**Firebase Admin SDK App Options:** + +The following Firebase Admin SDK `AppOptions` are supported and can be passed in `settings.fcm`: -- `settings.fcm.appName` [firebase app name](https://firebase.google.com/docs/reference/admin/node/firebase-admin.app.app#appname) (required) -- `settings.fcm.serviceAccountKey` [firebase service account file](https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments) use downloaded 'service-account-file.json' -- `settings.fcm.credential` [firebase credential](https://firebase.google.com/docs/reference/admin/node/firebase-admin.app.credential) - Note: one of `serviceAccountKey`, `credential` fcm options is required +- `appName` - [Firebase app name](https://firebase.google.com/docs/reference/admin/node/firebase-admin.app.app#appname) (required) +- `serviceAccountKey` - [Firebase service account file](https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments) use downloaded 'service-account-file.json' +- `credential` - [Firebase credential](https://firebase.google.com/docs/reference/admin/node/firebase-admin.app.credential) (one of `serviceAccountKey` or `credential` is required) +- `projectId` - Explicitly set the Google Cloud project ID (optional) +- `databaseURL` - Realtime Database URL (optional) +- `storageBucket` - Cloud Storage bucket name (optional) +- `serviceAccountId` - Service account email (optional) +- `databaseAuthVariableOverride` - Auth variable override for Realtime Database (optional) +- `httpAgent` - HTTP Agent for proxy support (optional, see [Proxy](#proxy) section) +- `httpsAgent` - HTTPS Agent for proxy support (optional, see [Proxy](#proxy) section) ```js const tokens = ['e..Gwso:APA91.......7r910HljzGUVS_f...kbyIFk2sK6......D2s6XZWn2E21x']; @@ -579,11 +514,7 @@ pushNotifications.send(tokens, notifications, (error, result) => { }); ``` -`fcm_notification` - object that will be passed to - -```js - new gcm.Message({ ..., notification: data.fcm_notification }) -``` +`fcm_notification` - object that will be passed to FCM message notification field Fcm object that will be sent to provider ([Fcm message format](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?authuser=0#Message)) : @@ -687,23 +618,58 @@ A working server example implementation can be found at [https://github.com/alex ## Proxy -To use the module with a proxy: +The module supports proxy configuration at two different levels: -``` +### Network Proxy (SDK-level) + +To route Firebase Admin SDK network requests through a corporate proxy, configure HTTP/HTTPS agents: + +```javascript +import { HttpProxyAgent } from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; -... + const settings = { - fcm: { - ..., - httpAgent = new HttpsProxyAgent(`http://${env.proxy.host}:${env.proxy.port}`); - }, - apn: { - ... - proxy: { - host: , - port: - } + fcm: { + appName: 'myApp', + credential: { ... }, + // Route all Firebase Admin SDK network traffic through proxy + httpAgent: new HttpProxyAgent(`http://${env.proxy.host}:${env.proxy.port}`), + httpsAgent: new HttpsProxyAgent(`http://${env.proxy.host}:${env.proxy.port}`), + }, +}; +``` + +This affects how the SDK communicates with Google's servers and applies to all Firebase services. + +### Notification Proxy Behavior (Android-level) + +To control how Android devices handle notifications in proxy scenarios, use the `proxy` property in the notification data: + +```javascript +const data = { + title: 'Notification', + body: 'Test', + proxy: 'allow', // Can be 'allow', 'deny', or 'if_priority_lowered' +}; + +push.send(registrationIds, data); +``` + +This is a notification-level setting that tells the Android system whether to deliver the notification when the device is on a proxy network. + +### Platform-Specific Proxy + +For APN (Apple Push Notification), configure the proxy at the app settings level: + +```javascript +const settings = { + apn: { + token: { ... }, + proxy: { + host: , + port: } + } }; ``` diff --git a/package-lock.json b/package-lock.json index ef7f9de..12dde6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,8 @@ "license": "MIT", "dependencies": { "@parse/node-apn": "7.0.1", - "firebase-admin": "12.7.0", + "firebase-admin": "13.6.0", "node-adm": "0.9.1", - "node-gcm": "1.1.4", "web-push": "3.6.7", "wns": "0.5.4" }, @@ -37,7 +36,7 @@ "sinon-chai": "3.5.0" }, "engines": { - "node": ">=14.x.x" + "node": ">=18.x.x" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1858,96 +1857,114 @@ "license": "MIT" }, "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", - "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", "license": "Apache-2.0" }, "node_modules/@firebase/app-types": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", - "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", "license": "Apache-2.0" }, "node_modules/@firebase/auth-interop-types": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", - "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", "license": "Apache-2.0" }, "node_modules/@firebase/component": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.9.tgz", - "integrity": "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.10.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@firebase/database": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.8.tgz", - "integrity": "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", "license": "Apache-2.0", "dependencies": { - "@firebase/app-check-interop-types": "0.3.2", - "@firebase/auth-interop-types": "0.2.3", - "@firebase/component": "0.6.9", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@firebase/database-compat": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.8.tgz", - "integrity": "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.9", - "@firebase/database": "1.0.8", - "@firebase/database-types": "1.0.5", - "@firebase/logger": "0.4.2", - "@firebase/util": "1.10.0", + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@firebase/database-types": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.5.tgz", - "integrity": "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==", + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", "license": "Apache-2.0", "dependencies": { - "@firebase/app-types": "0.9.2", - "@firebase/util": "1.10.0" + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" } }, "node_modules/@firebase/logger": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", - "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@firebase/util": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.0.tgz", - "integrity": "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@google-cloud/firestore": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.8.0.tgz", - "integrity": "sha512-m21BWVZLz7H7NF8HZ5hCGUSCEJKNwYB5yzQqDTuE9YUzNDRMDei3BwVDht5k4xF636sGlnobyBL+dcbthSGONg==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", + "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==", + "license": "Apache-2.0", "optional": true, "dependencies": { + "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", "google-gax": "^4.3.3", @@ -1961,6 +1978,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", "optional": true, "dependencies": { "arrify": "^2.0.0", @@ -1974,6 +1992,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", "optional": true, "engines": { "node": ">=14.0.0" @@ -1983,24 +2002,26 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", "optional": true, "engines": { "node": ">=14" } }, "node_modules/@google-cloud/storage": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.11.2.tgz", - "integrity": "sha512-jJOrKyOdujfrSF8EJODW9yY6hqO4jSTk6eVITEj2gsD43BSXuDlnMlLOaBUQhXL29VGnSkxDgYl5tlFhA6LKSA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.18.0.tgz", + "integrity": "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "^4.0.0", + "@google-cloud/promisify": "<4.1.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", - "fast-xml-parser": "^4.3.0", + "fast-xml-parser": "^4.4.1", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", @@ -2018,6 +2039,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", "optional": true, "dependencies": { "yocto-queue": "^0.1.0" @@ -2033,40 +2055,50 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", "optional": true, "bin": { "uuid": "dist/bin/uuid" } }, - "node_modules/@google-cloud/storage/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", "optional": true, - "engines": { - "node": ">=10" + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=12.10.0" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.10.9", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.9.tgz", - "integrity": "sha512-5tcgUctCG0qoNyfChZifz2tJqbRbXVO9J7X6duFcOjY3HUNCxg5D0ZCK7EP9vIcZ0zRpLU9bWkyCqVCLZ46IbQ==", + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@grpc/proto-loader": "^0.7.13", - "@js-sdsl/ordered-map": "^4.4.2" + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" }, "engines": { - "node": ">=12.10.0" + "node": ">=6" } }, "node_modules/@grpc/proto-loader": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", - "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { "lodash.camelcase": "^4.3.0", @@ -2733,6 +2765,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", "optional": true, "funding": { "type": "opencollective", @@ -2957,6 +2990,16 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@parse/node-apn": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-7.0.1.tgz", @@ -3027,30 +3070,35 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.1", @@ -3061,30 +3109,35 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause", "optional": true }, "node_modules/@rtsao/scc": { @@ -3105,6 +3158,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", "optional": true, "engines": { "node": ">= 10" @@ -3130,6 +3184,7 @@ "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", "optional": true }, "node_modules/@types/color-name": { @@ -3205,6 +3260,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT", "optional": true }, "node_modules/@types/mime": { @@ -3213,9 +3269,9 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "version": "22.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", + "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3239,31 +3295,57 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/request": { - "version": "2.48.12", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", - "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "version": "2.48.13", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", + "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "license": "MIT", "optional": true, "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", - "form-data": "^2.5.0" + "form-data": "^2.5.5" } }, "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "license": "MIT", "optional": true, "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" }, "engines": { "node": ">= 0.12" } }, + "node_modules/@types/request/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -3287,6 +3369,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", "optional": true }, "node_modules/abort-controller": { @@ -3586,6 +3669,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", "optional": true, "engines": { "node": ">=8" @@ -3661,7 +3745,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "devOptional": true }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -3694,29 +3779,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", @@ -3779,7 +3841,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true, "funding": [ { "type": "github", @@ -3833,10 +3894,10 @@ "license": "Apache-2.0" }, "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "optional": true, + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", "engines": { "node": "*" } @@ -4070,7 +4131,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4358,6 +4419,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "devOptional": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4748,6 +4810,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -4792,7 +4855,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4807,6 +4870,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", "optional": true, "dependencies": { "end-of-stream": "^1.4.1", @@ -4854,9 +4918,10 @@ "devOptional": true }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", "optional": true, "dependencies": { "once": "^1.4.0" @@ -4935,7 +5000,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4945,7 +5010,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, + "devOptional": true, "engines": { "node": ">= 0.4" } @@ -4954,7 +5019,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4967,7 +5032,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5518,8 +5583,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "devOptional": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extsprintf": { "version": "1.4.0", @@ -5558,8 +5622,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.2.0", @@ -5580,22 +5643,19 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", - "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], + "license": "MIT", "optional": true, "dependencies": { - "strnum": "^1.0.5" + "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" @@ -5742,53 +5802,43 @@ "node": ">=8" } }, - "node_modules/find-up/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/firebase-admin": { - "version": "12.7.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz", - "integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==", + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.0.tgz", + "integrity": "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw==", "license": "Apache-2.0", "dependencies": { "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "1.0.8", - "@firebase/database-types": "1.0.5", - "@types/node": "^22.0.1", + "@firebase/database-compat": "^2.0.0", + "@firebase/database-types": "^1.0.6", + "@types/node": "^22.8.7", "farmhash-modern": "^1.1.0", + "fast-deep-equal": "^3.1.1", + "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", - "uuid": "^10.0.0" + "uuid": "^11.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "optionalDependencies": { - "@google-cloud/firestore": "^7.7.0", - "@google-cloud/storage": "^7.7.0" + "@google-cloud/firestore": "^7.11.0", + "@google-cloud/storage": "^7.14.0" } }, "node_modules/firebase-admin/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/flat": { @@ -5821,25 +5871,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5938,7 +5969,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, + "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5968,6 +5999,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", "optional": true }, "node_modules/functions-have-names": { @@ -5981,10 +6013,10 @@ } }, "node_modules/gaxios": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz", - "integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==", - "optional": true, + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", @@ -5996,26 +6028,6 @@ "node": ">=14" } }, - "node_modules/gaxios/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/gaxios/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -6024,18 +6036,19 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "optional": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/gcp-metadata": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", - "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", - "optional": true, + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", "dependencies": { - "gaxios": "^6.0.0", + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" }, "engines": { @@ -6097,7 +6110,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6131,7 +6144,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6312,10 +6325,10 @@ } }, "node_modules/google-auth-library": { - "version": "9.11.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.11.0.tgz", - "integrity": "sha512-epX3ww/mNnhl6tL45EQ/oixsY8JLEgUFoT4A5E/5iAR4esld9Kqv6IJGk7EmGuOgDvaarwF95hU2+v7Irql9lw==", - "optional": true, + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", @@ -6332,7 +6345,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "optional": true, + "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -6343,28 +6356,29 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "optional": true, + "license": "MIT", "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "node_modules/google-gax": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.6.tgz", - "integrity": "sha512-z3MR+pE6WqU+tnKtkJl4c723EYY7Il4fcSNgEbehzUJpcNWkca9AyoC2pdBWmEa0cda21VRpUBb4s6VSATiUKg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", + "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@grpc/grpc-js": "~1.10.3", + "@grpc/grpc-js": "^1.10.9", "@grpc/proto-loader": "^0.7.13", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", "duplexify": "^4.0.0", "google-auth-library": "^9.3.0", - "node-fetch": "^2.6.1", + "node-fetch": "^2.7.0", "object-hash": "^3.0.0", - "proto3-json-serializer": "^2.0.0", - "protobufjs": "7.3.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", "retry-request": "^7.0.0", "uuid": "^9.0.1" }, @@ -6372,50 +6386,6 @@ "node": ">=14" } }, - "node_modules/google-gax/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/google-gax/node_modules/protobufjs": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", - "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/google-gax/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -6424,16 +6394,26 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "optional": true, "bin": { "uuid": "dist/bin/uuid" } }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6452,7 +6432,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "optional": true, + "license": "MIT", "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" @@ -6465,7 +6445,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "optional": true, + "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -6476,7 +6456,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "optional": true, + "license": "MIT", "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" @@ -6549,7 +6529,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6562,7 +6542,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6591,7 +6571,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, + "devOptional": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6609,9 +6589,9 @@ } }, "node_modules/html-entities": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", - "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", "funding": [ { "type": "github", @@ -6622,6 +6602,7 @@ "url": "https://patreon.com/mdevils" } ], + "license": "MIT", "optional": true }, "node_modules/html-escaper": { @@ -7223,7 +7204,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "devOptional": true, "engines": { "node": ">=8" } @@ -7654,7 +7634,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "optional": true, + "license": "MIT", "dependencies": { "bignumber.js": "^9.0.0" } @@ -7844,12 +7824,14 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", "optional": true }, "node_modules/lodash.capitalize": { @@ -7987,9 +7969,10 @@ "dev": true }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", "optional": true }, "node_modules/loupe": { @@ -8052,7 +8035,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8082,6 +8065,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", "optional": true, "bin": { "mime": "cli.js" @@ -8094,6 +8078,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, "engines": { "node": ">= 0.6" } @@ -8102,6 +8087,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -8429,6 +8415,26 @@ "node": ">= 0.6.0" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -8445,27 +8451,6 @@ "node": ">= 6.13.0" } }, - "node_modules/node-gcm": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/node-gcm/-/node-gcm-1.1.4.tgz", - "integrity": "sha512-6Z3Ksmum3xsux/Ejwg2pn+yELvL13nIP5ZbdJDZupnipfP10xyPvJGt5jlB3pCrKkIzcTKL87OI0xsbbz8YkpA==", - "dependencies": { - "axios": "~1.6.8", - "debug": "^3.1.0", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/node-gcm/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -8778,6 +8763,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", "optional": true, "engines": { "node": ">= 6" @@ -9491,6 +9477,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { "protobufjs": "^7.2.5" @@ -9500,10 +9487,11 @@ } }, "node_modules/protobufjs": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", - "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", "hasInstallScript": true, + "license": "BSD-3-Clause", "optional": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -9563,7 +9551,8 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true }, "node_modules/psl": { "version": "1.2.0", @@ -9611,9 +9600,10 @@ } }, "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "optional": true, "dependencies": { "inherits": "^2.0.3", @@ -10016,6 +10006,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", "optional": true, "dependencies": { "@types/request": "^2.48.8", @@ -10525,6 +10516,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", "optional": true, "dependencies": { "stubs": "^3.0.0" @@ -10534,6 +10526,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", "optional": true }, "node_modules/string_decoder": { @@ -10711,9 +10704,16 @@ } }, "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", "optional": true }, "node_modules/strtok3": { @@ -10738,6 +10738,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", "optional": true }, "node_modules/supports-color": { @@ -10793,6 +10794,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", "optional": true, "dependencies": { "http-proxy-agent": "^5.0.0", @@ -10809,6 +10811,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", "optional": true, "dependencies": { "debug": "4" @@ -10821,6 +10824,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", "optional": true, "dependencies": { "@tootallnate/once": "2", @@ -10835,6 +10839,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", "optional": true, "dependencies": { "agent-base": "6", @@ -10844,26 +10849,6 @@ "node": ">= 6" } }, - "node_modules/teeny-request/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/teeny-request/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -10872,6 +10857,7 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "optional": true, "bin": { "uuid": "dist/bin/uuid" @@ -11054,7 +11040,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "optional": true + "license": "MIT" }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -11385,7 +11371,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", "optional": true }, "node_modules/util/node_modules/inherits": { @@ -11458,7 +11445,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "optional": true + "license": "BSD-2-Clause" }, "node_modules/websocket-driver": { "version": "0.7.4", @@ -11487,7 +11474,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "optional": true, + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -11801,6 +11788,19 @@ "node": ">=10" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", diff --git a/package.json b/package.json index ca8da6b..f620017 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,8 @@ }, "dependencies": { "@parse/node-apn": "7.0.1", - "firebase-admin": "12.7.0", + "firebase-admin": "13.6.0", "node-adm": "0.9.1", - "node-gcm": "1.1.4", "web-push": "3.6.7", "wns": "0.5.4" }, @@ -79,7 +78,7 @@ "sinon-chai": "3.5.0" }, "engines": { - "node": ">=14.x.x" + "node": ">=18.x.x" }, "eslintConfig": { "ecmaVersion": 6, diff --git a/src/constants.js b/src/constants.js index 8ad39da..9bf1966 100644 --- a/src/constants.js +++ b/src/constants.js @@ -2,7 +2,6 @@ module.exports = { DEFAULT_TTL: 28 * 86400, GCM_MAX_TTL: 2419200, // 4 weeks in seconds (https://firebase.google.com/docs/cloud-messaging/http-server-ref#downstream-http-messages-json) APN_METHOD: 'apn', - GCM_METHOD: 'gcm', FCM_METHOD: 'fcm', ADM_METHOD: 'adm', WNS_METHOD: 'wns', @@ -71,6 +70,5 @@ module.exports = { // contentEncoding: '< Encoding type, e.g.: aesgcm or aes128gcm >' }, isAlwaysUseFCM: false, - isLegacyGCM: false, }, }; diff --git a/src/push-notifications.js b/src/push-notifications.js index efbe78f..d250e57 100644 --- a/src/push-notifications.js +++ b/src/push-notifications.js @@ -1,4 +1,3 @@ -const sendGCM = require('./sendGCM'); const sendFCM = require('./sendFCM'); const APN = require('./sendAPN'); const sendADM = require('./sendADM'); @@ -11,7 +10,6 @@ const { WEB_METHOD, WNS_METHOD, ADM_METHOD, - GCM_METHOD, FCM_METHOD, APN_METHOD, } = require('./constants'); @@ -27,7 +25,6 @@ class PN { this.apn.shutdown(); } this.apn = new APN(this.settings.apn); - this.useFcmOrGcmMethod = this.settings.isLegacyGCM ? GCM_METHOD : FCM_METHOD; } sendWith(method, regIds, data, cb) { @@ -50,14 +47,14 @@ class PN { if (typeof regId === 'object' && regId.id && regId.type) { return { regId: regId.id, - pushMethod: this.settings.isAlwaysUseFCM ? this.useFcmOrGcmMethod : regId.type, + pushMethod: this.settings.isAlwaysUseFCM ? FCM_METHOD : regId.type, }; } // TODO: deprecated, remove of all cases below in v3.0 // and review test cases if (this.settings.isAlwaysUseFCM) { - return { regId, pushMethod: this.useFcmOrGcmMethod }; + return { regId, pushMethod: FCM_METHOD }; } if (regId.substring(0, 4) === 'http') { @@ -73,7 +70,7 @@ class PN { } if (regId.length > 64) { - return { regId, pushMethod: this.useFcmOrGcmMethod }; + return { regId, pushMethod: FCM_METHOD }; } return { regId, pushMethod: UNKNOWN_METHOD }; @@ -81,7 +78,6 @@ class PN { send(_regIds, data, callback) { const promises = []; - const regIdsGCM = []; const regIdsFCM = []; const regIdsAPN = []; const regIdsWNS = []; @@ -96,8 +92,6 @@ class PN { if (pushMethod === WEB_METHOD) { regIdsWebPush.push(regId); - } else if (pushMethod === GCM_METHOD) { - regIdsGCM.push(regId); } else if (pushMethod === FCM_METHOD) { regIdsFCM.push(regId); } else if (pushMethod === WNS_METHOD) { @@ -112,11 +106,6 @@ class PN { }); try { - // Android GCM / FCM (Android/iOS) Legacy - if (regIdsGCM.length > 0) { - promises.push(this.sendWith(sendGCM, regIdsGCM, data)); - } - // FCM (Android/iOS) if (regIdsFCM.length > 0) { promises.push(this.sendWith(sendFCM, regIdsFCM, data)); @@ -192,5 +181,4 @@ module.exports = PN; module.exports.WEB = WEB_METHOD; module.exports.WNS = WNS_METHOD; module.exports.ADM = ADM_METHOD; -module.exports.GCM = GCM_METHOD; module.exports.APN = APN_METHOD; diff --git a/src/sendFCM.js b/src/sendFCM.js index e005e18..69b6f67 100644 --- a/src/sendFCM.js +++ b/src/sendFCM.js @@ -70,9 +70,24 @@ const sendFCM = (regIds, data, settings) => { const opts = { credential: settings.fcm.credential || firebaseAdmin.credential.cert(settings.fcm.serviceAccountKey), - httpAgent: settings.fcm.httpAgent || undefined, }; + // Add optional Firebase AppOptions properties if provided + const optionalProps = [ + 'httpAgent', + 'httpsAgent', + 'projectId', + 'databaseURL', + 'storageBucket', + 'serviceAccountId', + 'databaseAuthVariableOverride', + ]; + optionalProps.forEach((prop) => { + if (settings.fcm[prop] !== undefined) { + opts[prop] = settings.fcm[prop]; + } + }); + const firebaseApp = firebaseAdmin.initializeApp(opts, appName); firebaseAdmin.INTERNAL.appStore.removeApp(appName); diff --git a/src/sendGCM.js b/src/sendGCM.js deleted file mode 100644 index 4aa4212..0000000 --- a/src/sendGCM.js +++ /dev/null @@ -1,108 +0,0 @@ -const gcm = require('node-gcm'); -const { GCM_METHOD } = require('./constants'); -const { containsValidRecipients, buildGcmMessage } = require('./utils/tools'); - -const getRecipientList = (obj) => obj.registrationTokens ?? [obj.to, obj.condition].filter(Boolean); - -const sendChunk = (GCMSender, recipients, message, retries) => - new Promise((resolve) => { - const recipientList = getRecipientList(recipients); - - GCMSender.send(message, recipients, retries, (err, response) => { - // Response: see https://developers.google.com/cloud-messaging/http-server-ref#table5 - if (err) { - resolve({ - method: GCM_METHOD, - success: 0, - failure: recipientList.length, - message: recipientList.map((value) => ({ - originalRegId: value, - regId: value, - error: err, - errorMsg: err instanceof Error ? err.message : err, - })), - }); - } else if (response && response.results !== undefined) { - let regIndex = 0; - resolve({ - method: GCM_METHOD, - multicastId: response.multicast_id, - success: response.success, - failure: response.failure, - message: response.results.map((value) => { - const regToken = recipientList[regIndex]; - regIndex += 1; - const errorMsg = value.error ? value.error.message || value.error : null; - return { - messageId: value.message_id, - originalRegId: regToken, - regId: value.registration_id || regToken, - error: value.error ? new Error(value.error) : null, - errorMsg, - }; - }), - }); - } else { - resolve({ - method: GCM_METHOD, - multicastId: response.multicast_id, - success: response.success, - failure: response.failure, - message: recipientList.map((value) => ({ - originalRegId: value, - regId: value, - error: new Error('unknown'), - errorMsg: 'unknown', - })), - }); - } - }); - }); - -const sendGCM = (regIds, data, settings) => { - const opts = { ...settings.gcm }; - const { id } = opts; - delete opts.id; - const GCMSender = new gcm.Sender(id, opts); - const promises = []; - - const message = buildGcmMessage(data, opts); - - let chunk = 0; - - /* allow to override device tokens with custom `to` or `condition` field: - * https://github.com/ToothlessGear/node-gcm#recipients */ - if (containsValidRecipients(data)) { - promises.push(sendChunk(GCMSender, data.recipients, message, data.retries || 0)); - } else { - // Split tokens in 1.000 chunks, see https://developers.google.com/cloud-messaging/http-server-ref#table1 - do { - const registrationTokens = regIds.slice(chunk * 1000, (chunk + 1) * 1000); - promises.push(sendChunk(GCMSender, { registrationTokens }, message, data.retries || 0)); - chunk += 1; - } while (1000 * chunk < regIds.length); - } - - return Promise.all(promises).then((results) => { - const resumed = { - method: GCM_METHOD, - multicastId: [], - success: 0, - failure: 0, - message: [], - }; - - results.forEach((result) => { - if (result.multicastId) { - resumed.multicastId.push(result.multicastId); - } - resumed.success += result.success; - resumed.failure += result.failure; - resumed.message.push(...result.message); - }); - - return resumed; - }); -}; - -module.exports = sendGCM; diff --git a/src/utils/fcmMessage.js b/src/utils/fcmMessage.js index c6b3ac6..05bb8e3 100644 --- a/src/utils/fcmMessage.js +++ b/src/utils/fcmMessage.js @@ -1,4 +1,4 @@ -const { buildGcmMessage, buildApnsMessage } = require('./tools'); +const { buildAndroidMessage, buildApnsMessage } = require('./tools'); class FcmMessage { constructor(params) { @@ -27,19 +27,8 @@ class FcmMessage { } static buildAndroidMessage(params, options) { - const message = buildGcmMessage(params, options); - - const androidMessage = message.toJson(); - - androidMessage.ttl = androidMessage.time_to_live * 1000; + const androidMessage = buildAndroidMessage(params, options); androidMessage.data = this.normalizeDataParams(androidMessage.data); - - delete androidMessage.content_available; - delete androidMessage.mutable_content; - delete androidMessage.delay_while_idle; - delete androidMessage.time_to_live; - delete androidMessage.dry_run; - return androidMessage; } diff --git a/src/utils/tools.js b/src/utils/tools.js index 6ec8968..7d8dac2 100644 --- a/src/utils/tools.js +++ b/src/utils/tools.js @@ -1,5 +1,4 @@ const { Notification: ApnsMessage } = require('@parse/node-apn'); -const { Message: GcmMessage } = require('node-gcm'); const { DEFAULT_TTL, GCM_MAX_TTL } = require('../constants'); @@ -85,44 +84,53 @@ const containsValidRecipients = (obj) => { return checkTo || checkCondition; }; -const buildGcmNotification = (data) => { +const buildAndroidNotification = (data) => { const notification = data.fcm_notification || { title: data.title, body: data.body, icon: data.icon, - image: data.image, - picture: data.picture, - style: data.style, + imageUrl: data.image || data.picture, sound: data.sound, - badge: data.badge, - tag: data.tag, color: data.color, - click_action: data.clickAction || data.category, - body_loc_key: data.locKey, - body_loc_args: toJSONorUndefined(data.locArgs), - title_loc_key: data.titleLocKey, - title_loc_args: toJSONorUndefined(data.titleLocArgs), - android_channel_id: data.android_channel_id, - notification_count: data.notificationCount || data.badge, + tag: data.tag, + clickAction: data.clickAction || data.category, + bodyLocKey: data.locKey, + bodyLocArgs: toJSONorUndefined(data.locArgs), + titleLocKey: data.titleLocKey, + titleLocArgs: toJSONorUndefined(data.titleLocArgs), + channelId: data.android_channel_id, + notificationCount: data.notificationCount || data.badge, + // Additional Firebase Admin SDK properties + ticker: data.ticker, + sticky: data.sticky, + visibility: data.visibility, + priority: data.notificationPriority, + vibrateTimingsMillis: data.vibrateTimingsMillis, + defaultVibrateTimings: data.defaultVibrateTimings, + defaultSound: data.defaultSound, + lightSettings: data.lightSettings, + defaultLightSettings: data.defaultLightSettings, + eventTimestamp: data.eventTimestamp, + localOnly: data.localOnly, + proxy: data.proxy, }; - return notification; + // Remove undefined values + return Object.fromEntries( + Object.entries(notification).filter(([, value]) => value !== undefined) + ); }; -const buildGcmMessage = (data, options) => { - const notification = buildGcmNotification(data); +const buildAndroidMessage = (data, options) => { + const notification = buildAndroidNotification(data); let custom; if (typeof data.custom === 'string') { - custom = { - message: data.custom, - }; + custom = { message: data.custom }; } else if (typeof data.custom === 'object') { custom = { ...data.custom }; } else { - custom = { - data: data.custom, - }; + custom = { data: data.custom }; } custom.title = custom.title || data.title; @@ -134,19 +142,31 @@ const buildGcmMessage = (data, options) => { custom['content-available'] = 1; } - const message = new GcmMessage({ + const fcmAndroidMessage = { collapseKey: data.collapseKey, priority: data.priority === 'normal' ? 'normal' : 'high', - contentAvailable: data.silent ? true : data.contentAvailable || false, - delayWhileIdle: data.delayWhileIdle || false, - timeToLive: extractTimeToLive(data), + ttl: extractTimeToLive(data) * 1000, // Convert seconds to milliseconds for FCM restrictedPackageName: data.restrictedPackageName, - dryRun: data.dryRun || false, - data: options.phonegap === true ? Object.assign(custom, notification) : custom, - notification: options.phonegap === true || data.silent === true ? undefined : notification, - }); + directBootOk: data.directBootOk, + data: custom, + }; - return message; + // Only add notification if not silent mode + if (data.silent !== true && options.phonegap !== true) { + fcmAndroidMessage.notification = notification; + } + + // Add FCM options if provided + if (data.fcmOptions || data.analyticsLabel) { + fcmAndroidMessage.fcmOptions = { + analyticsLabel: data.fcmOptions?.analyticsLabel || data.analyticsLabel, + }; + } + + // Remove undefined values + return Object.fromEntries( + Object.entries(fcmAndroidMessage).filter(([, value]) => value !== undefined) + ); }; const buildApnsMessage = (data) => { @@ -186,5 +206,5 @@ module.exports = { containsValidRecipients, buildApnsMessage, - buildGcmMessage, + buildAndroidMessage, }; diff --git a/test/push-notifications/basic.js b/test/push-notifications/basic.js index 01ddd3a..692a352 100644 --- a/test/push-notifications/basic.js +++ b/test/push-notifications/basic.js @@ -1,15 +1,15 @@ +/* eslint-env mocha */ import path from 'path'; import chai from 'chai'; import dirtyChai from 'dirty-chai'; import sinonChai from 'sinon-chai'; import { spy } from 'sinon'; -import PN from '../../src'; +import PN from '../../src/index.js'; import { UNKNOWN_METHOD, WEB_METHOD, WNS_METHOD, ADM_METHOD, - GCM_METHOD, FCM_METHOD, APN_METHOD, } from '../../src/constants'; @@ -35,9 +35,6 @@ describe('push-notifications: instantiation and class properties', () => { describe('override options with constructor', () => { let pn; const settings = { - gcm: { - id: 'gcm id', - }, fcm: { name: 'testAppName', // eslint-disable-next-line no-undef @@ -80,7 +77,6 @@ describe('push-notifications: instantiation and class properties', () => { it('should override the given options', () => { expect(pn.settings.apn).to.eql(settings.apn); - expect(pn.settings.gcm).to.eql(settings.gcm); expect(pn.settings.fcm).to.eql(settings.fcm); expect(pn.settings.adm).to.eql(settings.adm); expect(pn.settings.wns).to.eql(settings.wns); @@ -190,40 +186,39 @@ describe('push-notifications: instantiation and class properties', () => { unknownRegId: 'abcdef', }; - it('Android / GCM', () => { - let pn = new PN({ isLegacyGCM: true }); + it('Android / FCM', () => { + let pn = new PN({}); expect(pn.getPushMethodByRegId(regIds.androidRegId).regId).to.equal(regIds.androidRegId); - expect(pn.getPushMethodByRegId(regIds.androidRegId).pushMethod).to.equal(GCM_METHOD); + expect(pn.getPushMethodByRegId(regIds.androidRegId).pushMethod).to.equal(FCM_METHOD); expect(pn.getPushMethodByRegId(regIds.androidWithAdmSubstringRegId).regId).to.equal( regIds.androidWithAdmSubstringRegId ); expect(pn.getPushMethodByRegId(regIds.androidWithAdmSubstringRegId).pushMethod).to.equal( - GCM_METHOD + FCM_METHOD ); expect(pn.getPushMethodByRegId(regIds.androidWithAmznSubscringRegId).regId).to.equal( regIds.androidWithAmznSubscringRegId ); expect(pn.getPushMethodByRegId(regIds.androidWithAmznSubscringRegId).pushMethod).to.equal( - GCM_METHOD + FCM_METHOD ); const settings = { isAlwaysUseFCM: true, - isLegacyGCM: true, }; pn = new PN(settings); expect(pn.getPushMethodByRegId(regIds.unknownRegId).regId).to.equal(regIds.unknownRegId); - expect(pn.getPushMethodByRegId(regIds.unknownRegId).pushMethod).to.equal(GCM_METHOD); + expect(pn.getPushMethodByRegId(regIds.unknownRegId).pushMethod).to.equal(FCM_METHOD); expect(pn.getPushMethodByRegId(regIds.androidObject).regId).to.equal(regIds.androidObject.id); - expect(pn.getPushMethodByRegId(regIds.androidObject).pushMethod).to.equal(GCM_METHOD); + expect(pn.getPushMethodByRegId(regIds.androidObject).pushMethod).to.equal(FCM_METHOD); expect(pn.getPushMethodByRegId(regIds.androidObjectWhatever).regId).to.equal( regIds.androidObjectWhatever.id ); - expect(pn.getPushMethodByRegId(regIds.androidObjectWhatever).pushMethod).to.equal(GCM_METHOD); + expect(pn.getPushMethodByRegId(regIds.androidObjectWhatever).pushMethod).to.equal(FCM_METHOD); }); it('Android / FCM', () => { @@ -233,7 +228,6 @@ describe('push-notifications: instantiation and class properties', () => { // eslint-disable-next-line no-undef serviceAccountKey: require(path.resolve('test/send/FCM-service-account-key.json')), }, - isLegacyGCM: false, }; const pn = new PN(settings); diff --git a/test/push-notifications/regIds.js b/test/push-notifications/regIds.js index 99ab511..42d953c 100644 --- a/test/push-notifications/regIds.js +++ b/test/push-notifications/regIds.js @@ -1,10 +1,10 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import PN from '../../src'; -import sendGCM from '../../src/sendGCM'; -import sendADM from '../../src/sendADM'; -import sendWNS from '../../src/sendWNS'; -import sendWeb from '../../src/sendWeb'; +import PN from '../../src/index.js'; +import sendFCM from '../../src/sendFCM.js'; +import sendADM from '../../src/sendADM.js'; +import sendWNS from '../../src/sendWNS.js'; +import sendWeb from '../../src/sendWeb.js'; const regIds = [ 'APA91bFQCD9Ndd8uVggMhj1usfeWsKIfGyBUWMprpZLGciWrMjS-77bIY24IMQNeEHzjidCcddnDxqYo-UEV03xw6ySmtIgQyzTqhSxhPGAi1maf6KDMAQGuUWc6L5Khze8YK9YrL9I_WD1gl49P3f_9hr08ZAS5Tw', // android @@ -33,7 +33,7 @@ describe('push-notifications: call with registration ids for android, ios, windo let sendWith; before(() => { - pn = new PN({ isLegacyGCM: true }); + pn = new PN({}); const sendApnFunctionName = pn.apn.sendAPN.bind(pn.apn).name; sendWith = sinon.stub(PN.prototype, 'sendWith', (method, _regIds, _data, cb) => { @@ -47,7 +47,7 @@ describe('push-notifications: call with registration ids for android, ios, windo case 0: case 1: case 2: - expect(method).to.equal(sendGCM); + expect(method).to.equal(sendFCM); break; case 3: @@ -68,7 +68,7 @@ describe('push-notifications: call with registration ids for android, ios, windo break; default: - expect.fail(null, null, 'Method should be sendGCM, sendAPN, sendWNS, sendADM or sendWeb'); + expect.fail(null, null, 'Method should be sendFCM, sendAPN, sendWNS, sendADM or sendWeb'); break; } expect(data).to.equal(data); @@ -103,7 +103,7 @@ describe('push-notifications: call with registration ids for android, ios, windo const assertPushResultsForArrayInput = (result) => { let expectedNumRegIds = 1; - if (result.method === 'sendGCM') { + if (result.method === 'sendFCM') { expectedNumRegIds = 3; } else if (result.method === 'bound sendAPN') { expectedNumRegIds = 2; diff --git a/test/send/sendADM.js b/test/send/sendADM.js index 1b829bf..c781ad9 100644 --- a/test/send/sendADM.js +++ b/test/send/sendADM.js @@ -1,8 +1,9 @@ +/* eslint-env mocha */ import { expect } from 'chai'; import sinon from 'sinon'; import adm from 'node-adm'; -import PN from '../../src'; -import { sendOkMethodGCM, testPushSuccess, testPushError, testPushException } from '../util'; +import PN from '../../src/index.js'; +import { testPushSuccess, testPushError, testPushException } from '../util.js'; const method = 'adm'; const regIds = [ @@ -28,7 +29,6 @@ const pn = new PN(admOpts); const fErr = new Error('Forced error'); const testSuccess = testPushSuccess(method, regIds); -const testSuccessGCM = testPushSuccess('gcm', regIds); const testError = testPushError(method, regIds, fErr.message); const testException = testPushException(fErr.message); @@ -150,29 +150,4 @@ describe('push-notifications-adm', () => { .catch((err) => testException(err, undefined, done)); }); }); - - describe('send push notifications using FCM', () => { - const pnGCM = new PN({ - isAlwaysUseFCM: true, - isLegacyGCM: true, - }); - before(() => { - sendMethod = sendOkMethodGCM(regIds, data); - }); - - after(() => { - sendMethod.restore(); - }); - - it('all responses should be successful (callback)', (done) => { - pnGCM.send(regIds, data, (err, results) => testSuccessGCM(err, results, done)); - }); - - it('all responses should be successful (promise)', (done) => { - pnGCM - .send(regIds, data) - .then((results) => testSuccessGCM(null, results, done)) - .catch(done); - }); - }); }); diff --git a/test/send/sendAPN.js b/test/send/sendAPN.js index 89a09ea..4ffa9d9 100644 --- a/test/send/sendAPN.js +++ b/test/send/sendAPN.js @@ -4,10 +4,10 @@ import sinonChai from 'sinon-chai'; import dirtyChai from 'dirty-chai'; import apn from '@parse/node-apn'; -import PN from '../../src'; -import APN from '../../src/sendAPN'; +import PN from '../../src/index.js'; +import APN from '../../src/sendAPN.js'; -import { sendOkMethodGCM, testPushSuccess, testPushError, testPushException } from '../util'; +import { testPushSuccess, testPushError, testPushException } from '../util.js'; // Mock apn certificate loading to prevent file access before(() => { @@ -55,7 +55,6 @@ const fErr = new Error('Forced error'); const errStatusCode = '410'; const testSuccess = testPushSuccess(method, regIds); -const testSuccessGCM = testPushSuccess('gcm', regIds); const testError = testPushError(method, regIds, fErr.message); const testErrorStatusCode = testPushError(method, regIds, errStatusCode); const testException = testPushException(fErr.message); @@ -675,29 +674,4 @@ describe('push-notifications-apn', () => { }); }); }); - - describe('send push notifications successfully using FCM', () => { - const pnGCM = new PN({ - isAlwaysUseFCM: true, - isLegacyGCM: true, - }); - before(() => { - sendMethod = sendOkMethodGCM(regIds, data); - }); - - after(() => { - sendMethod.restore(); - }); - - it('all responses should be successful (callback)', (done) => { - pnGCM.send(regIds, data, (err, results) => testSuccessGCM(err, results, done)); - }); - - it('all responses should be successful (promise)', (done) => { - pnGCM - .send(regIds, data) - .then((results) => testSuccessGCM(null, results, done)) - .catch(done); - }); - }); }); diff --git a/test/send/sendFCM.js b/test/send/sendFCM.js index 23caae3..4f7e9ae 100644 --- a/test/send/sendFCM.js +++ b/test/send/sendFCM.js @@ -1,8 +1,10 @@ +/* eslint-env mocha */ import { expect } from 'chai'; import sinon from 'sinon'; import { Messaging as fbMessaging } from 'firebase-admin/messaging'; -import PN from '../../src'; -import { testPushSuccess } from '../util'; +import * as firebaseAdmin from 'firebase-admin'; +import PN from '../../src/index.js'; +import { testPushSuccess } from '../util.js'; const method = 'fcm'; const regIds = [ @@ -12,6 +14,22 @@ const message = { title: 'title', body: 'body', sound: 'mySound.aiff', + icon: 'testIcon', + color: '#FF0000', + clickAction: 'OPEN_ACTIVITY_1', + android_channel_id: 'test_channel', + tag: 'test-tag', + badge: 5, + ticker: 'test-ticker', + sticky: true, + visibility: 'public', + localOnly: false, + eventTimestamp: new Date('2026-01-07T12:00:00Z'), + notificationPriority: 'high', + vibrateTimingsMillis: [100, 200, 100], + defaultVibrateTimings: false, + defaultSound: true, + analyticsLabel: 'test_analytics', custom: { sender: 'banshi-test', }, @@ -21,7 +39,6 @@ const fcmOpts = { name: 'testAppName', credential: { getAccessToken: () => Promise.resolve({}) }, }, - isLegacyGCM: false, }; const pn = new PN(fcmOpts); @@ -34,18 +51,41 @@ function sendOkMethod() { fbMessaging.prototype, 'sendEachForMulticast', function sendFCM(firebaseMessage) { - const { custom, ...messageData } = message; + const { custom, analyticsLabel, android_channel_id, ...notificationData } = message; expect(firebaseMessage.tokens).to.deep.equal(regIds); expect(firebaseMessage.android.priority).to.equal('high'); - expect(firebaseMessage.android.notification).to.deep.include(messageData); + expect(firebaseMessage.android.notification).to.deep.include({ + title: notificationData.title, + body: notificationData.body, + sound: notificationData.sound, + icon: notificationData.icon, + color: notificationData.color, + clickAction: notificationData.clickAction, + channelId: android_channel_id, + tag: notificationData.tag, + notificationCount: notificationData.badge, + ticker: notificationData.ticker, + sticky: notificationData.sticky, + visibility: notificationData.visibility, + localOnly: notificationData.localOnly, + eventTimestamp: notificationData.eventTimestamp, + priority: notificationData.notificationPriority, + vibrateTimingsMillis: notificationData.vibrateTimingsMillis, + defaultVibrateTimings: notificationData.defaultVibrateTimings, + defaultSound: notificationData.defaultSound, + }); + + expect(firebaseMessage.android.fcmOptions).to.deep.equal({ + analyticsLabel: analyticsLabel, + }); - expect(firebaseMessage.apns.payload.aps.sound).to.equal(messageData.sound); + expect(firebaseMessage.apns.payload.aps.sound).to.equal(notificationData.sound); expect(firebaseMessage.apns.payload.aps.alert).to.deep.include({ - title: messageData.title, - body: messageData.body, + title: notificationData.title, + body: notificationData.body, }); expect(firebaseMessage.data).to.deep.equal(custom); @@ -75,4 +115,358 @@ describe('push-notifications-fcm', () => { .catch(done); }); }); + + describe('proxy support', () => { + it('should accept httpAgent in settings', (done) => { + const mockHttpAgent = {}; + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const fcmOptsWithProxy = { + fcm: { + name: 'testAppNameProxy', + credential: { getAccessToken: () => Promise.resolve({}) }, + httpAgent: mockHttpAgent, + }, + }; + + const pnWithProxy = new PN(fcmOptsWithProxy); + + pnWithProxy + .send(regIds, message) + .then(() => { + // Verify that initializeApp was called with httpAgent + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.httpAgent).to.equal(mockHttpAgent); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + + it('should accept httpsAgent in settings', (done) => { + const mockHttpsAgent = {}; + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const fcmOptsWithProxy = { + fcm: { + name: 'testAppNameProxyHttps', + credential: { getAccessToken: () => Promise.resolve({}) }, + httpsAgent: mockHttpsAgent, + }, + }; + + const pnWithProxy = new PN(fcmOptsWithProxy); + + pnWithProxy + .send(regIds, message) + .then(() => { + // Verify that initializeApp was called with httpsAgent + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.httpsAgent).to.equal(mockHttpsAgent); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + + it('should accept both httpAgent and httpsAgent in settings', (done) => { + const mockHttpAgent = {}; + const mockHttpsAgent = {}; + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const fcmOptsWithProxy = { + fcm: { + name: 'testAppNameProxyBoth', + credential: { getAccessToken: () => Promise.resolve({}) }, + httpAgent: mockHttpAgent, + httpsAgent: mockHttpsAgent, + }, + }; + + const pnWithProxy = new PN(fcmOptsWithProxy); + + pnWithProxy + .send(regIds, message) + .then(() => { + // Verify that initializeApp was called with both agents + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.httpAgent).to.equal(mockHttpAgent); + expect(callArgs.httpsAgent).to.equal(mockHttpsAgent); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + }); + + describe('Firebase Admin SDK AppOptions support', () => { + it('should accept projectId in settings', (done) => { + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const fcmOptsWithProjectId = { + fcm: { + name: 'testAppNameProjectId', + credential: { getAccessToken: () => Promise.resolve({}) }, + projectId: 'my-firebase-project', + }, + }; + + const pnWithProjectId = new PN(fcmOptsWithProjectId); + + pnWithProjectId + .send(regIds, message) + .then(() => { + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.projectId).to.equal('my-firebase-project'); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + + it('should accept databaseURL in settings', (done) => { + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const fcmOptsWithDatabaseURL = { + fcm: { + name: 'testAppNameDatabaseURL', + credential: { getAccessToken: () => Promise.resolve({}) }, + databaseURL: 'https://my-database.firebaseio.com', + }, + }; + + const pnWithDatabaseURL = new PN(fcmOptsWithDatabaseURL); + + pnWithDatabaseURL + .send(regIds, message) + .then(() => { + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.databaseURL).to.equal('https://my-database.firebaseio.com'); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + + it('should accept storageBucket in settings', (done) => { + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const fcmOptsWithStorageBucket = { + fcm: { + name: 'testAppNameStorageBucket', + credential: { getAccessToken: () => Promise.resolve({}) }, + storageBucket: 'my-bucket.appspot.com', + }, + }; + + const pnWithStorageBucket = new PN(fcmOptsWithStorageBucket); + + pnWithStorageBucket + .send(regIds, message) + .then(() => { + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.storageBucket).to.equal('my-bucket.appspot.com'); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + + it('should accept serviceAccountId in settings', (done) => { + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const fcmOptsWithServiceAccountId = { + fcm: { + name: 'testAppNameServiceAccountId', + credential: { getAccessToken: () => Promise.resolve({}) }, + serviceAccountId: 'my-service@my-project.iam.gserviceaccount.com', + }, + }; + + const pnWithServiceAccountId = new PN(fcmOptsWithServiceAccountId); + + pnWithServiceAccountId + .send(regIds, message) + .then(() => { + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.serviceAccountId).to.equal( + 'my-service@my-project.iam.gserviceaccount.com' + ); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + + it('should accept databaseAuthVariableOverride in settings', (done) => { + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const authOverride = { uid: 'test-user-123' }; + const fcmOptsWithAuthOverride = { + fcm: { + name: 'testAppNameAuthOverride', + credential: { getAccessToken: () => Promise.resolve({}) }, + databaseAuthVariableOverride: authOverride, + }, + }; + + const pnWithAuthOverride = new PN(fcmOptsWithAuthOverride); + + pnWithAuthOverride + .send(regIds, message) + .then(() => { + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.databaseAuthVariableOverride).to.deep.equal(authOverride); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + + it('should accept multiple Firebase AppOptions together', (done) => { + const mockInitializeApp = sinon.stub(firebaseAdmin, 'initializeApp').returns({ + messaging: () => ({ + sendEachForMulticast: () => + Promise.resolve({ + successCount: 1, + failureCount: 0, + responses: [{ error: null }], + }), + }), + }); + + const mockHttpAgent = {}; + const authOverride = { uid: 'test-user-456' }; + const fcmOptsWithMultiple = { + fcm: { + name: 'testAppNameMultiple', + credential: { getAccessToken: () => Promise.resolve({}) }, + projectId: 'my-firebase-project', + databaseURL: 'https://my-database.firebaseio.com', + storageBucket: 'my-bucket.appspot.com', + serviceAccountId: 'my-service@my-project.iam.gserviceaccount.com', + databaseAuthVariableOverride: authOverride, + httpAgent: mockHttpAgent, + }, + }; + + const pnWithMultiple = new PN(fcmOptsWithMultiple); + + pnWithMultiple + .send(regIds, message) + .then(() => { + const callArgs = mockInitializeApp.getCall(0).args[0]; + expect(callArgs.projectId).to.equal('my-firebase-project'); + expect(callArgs.databaseURL).to.equal('https://my-database.firebaseio.com'); + expect(callArgs.storageBucket).to.equal('my-bucket.appspot.com'); + expect(callArgs.serviceAccountId).to.equal( + 'my-service@my-project.iam.gserviceaccount.com' + ); + expect(callArgs.databaseAuthVariableOverride).to.deep.equal(authOverride); + expect(callArgs.httpAgent).to.equal(mockHttpAgent); + mockInitializeApp.restore(); + done(); + }) + .catch((err) => { + mockInitializeApp.restore(); + done(err); + }); + }); + }); }); diff --git a/test/send/sendWEB.js b/test/send/sendWEB.js index a92e6b6..0888862 100644 --- a/test/send/sendWEB.js +++ b/test/send/sendWEB.js @@ -1,8 +1,9 @@ +/* eslint-env mocha */ import { expect } from 'chai'; import sinon from 'sinon'; import webpush from 'web-push'; -import PN from '../../src'; -import { testPushSuccess, testPushError, testPushException } from '../util'; +import PN from '../../src/index.js'; +import { testPushSuccess, testPushError, testPushException } from '../util.js'; const method = 'webPush'; const regIds = [ diff --git a/test/send/sendWNS-accessToken.js b/test/send/sendWNS-accessToken.js index 68b9d4f..bfc317d 100644 --- a/test/send/sendWNS-accessToken.js +++ b/test/send/sendWNS-accessToken.js @@ -1,8 +1,9 @@ +/* eslint-env mocha */ import { expect } from 'chai'; import sinon from 'sinon'; import wns from 'wns'; -import PN from '../../src'; -import { sendOkMethodGCM, testPushSuccess, testPushError, testPushException } from '../util'; +import PN from '../../src/index.js'; +import { testPushSuccess, testPushError, testPushException } from '../util.js'; const method = 'wns'; const regIds = [ @@ -86,7 +87,6 @@ const sendWNS = { }; const testSuccess = testPushSuccess(method, regIds); -const testSuccessGCM = testPushSuccess('gcm', regIds); const testError = testPushError(method, regIds, fErr.message); const testException = testPushException(fErr.message); @@ -205,29 +205,4 @@ describe('push-notifications-wns-access-token', () => { .catch((err) => testException(err, undefined, done)); }); }); - - describe('send push notifications successfully using FCM', () => { - const pnGCM = new PN({ - isAlwaysUseFCM: true, - isLegacyGCM: true, - }); - before(() => { - sendMethod = sendOkMethodGCM(regIds, data); - }); - - after(() => { - sendMethod.restore(); - }); - - it('all responses should be successful (callback)', (done) => { - pnGCM.send(regIds, data, (err, results) => testSuccessGCM(err, results, done)); - }); - - it('all responses should be successful (promise)', (done) => { - pnGCM - .send(regIds, data) - .then((results) => testSuccessGCM(null, results, done)) - .catch(done); - }); - }); }); diff --git a/test/send/sendWNS.js b/test/send/sendWNS.js index ca01f18..b9e61b2 100644 --- a/test/send/sendWNS.js +++ b/test/send/sendWNS.js @@ -1,8 +1,9 @@ +/* eslint-env mocha */ import { expect } from 'chai'; import sinon from 'sinon'; import wns from 'wns'; -import PN from '../../src'; -import { sendOkMethodGCM, testPushSuccess, testPushError, testPushException } from '../util'; +import PN from '../../src/index.js'; +import { testPushSuccess, testPushError, testPushException } from '../util.js'; const method = 'wns'; const regIds = [ @@ -81,7 +82,6 @@ const sendWNS = { }; const testSuccess = testPushSuccess(method, regIds); -const testSuccessGCM = testPushSuccess('gcm', regIds); const testError = testPushError(method, regIds, fErr.message); const testException = testPushException(fErr.message); @@ -236,29 +236,4 @@ describe('push-notifications-wns', () => { .catch((err) => testException(err, undefined, done)); }); }); - - describe('send push notifications successfully using FCM', () => { - const pnGCM = new PN({ - isAlwaysUseFCM: true, - isLegacyGCM: true, - }); - before(() => { - sendMethod = sendOkMethodGCM(regIds, data); - }); - - after(() => { - sendMethod.restore(); - }); - - it('all responses should be successful (callback)', (done) => { - pnGCM.send(regIds, data, (err, results) => testSuccessGCM(err, results, done)); - }); - - it('all responses should be successful (promise)', (done) => { - pnGCM - .send(regIds, data) - .then((results) => testSuccessGCM(null, results, done)) - .catch(done); - }); - }); }); diff --git a/test/util.js b/test/util.js index 3895c00..18fd5b3 100644 --- a/test/util.js +++ b/test/util.js @@ -1,5 +1,3 @@ -import sinon from 'sinon'; -import gcm from 'node-gcm'; import { expect } from 'chai'; export const testPushSuccess = (method, regIds) => (err, results, done) => { @@ -13,10 +11,6 @@ export const testPushSuccess = (method, regIds) => (err, results, done) => { result.message.forEach((message) => { expect(message).to.have.property('regId'); expect(regIds).to.include(message.regId); - if (method === 'gcm') { - expect(message).to.have.property('originalRegId'); - expect(regIds).to.include(message.originalRegId); - } }); }); done(err); @@ -59,34 +53,3 @@ export const testPushException = (errMessage) => (err, results, done) => { done(err || e); } }; - -export const sendOkMethodGCM = (regIds, data) => - sinon.stub(gcm.Sender.prototype, 'send', (message, recipients, retries, cb) => { - expect(recipients).to.be.instanceOf(Object); - expect(recipients).to.have.property('registrationTokens'); - const { registrationTokens } = recipients; - expect(registrationTokens).to.be.instanceOf(Array); - registrationTokens.forEach((regId) => expect(regIds).to.include(regId)); - expect(retries).to.be.a('number'); - expect(message).to.be.instanceOf(gcm.Message); - expect(message.params.notification.title).to.eql(data.title); - expect(message.params.notification.body).to.eql(data.body); - expect(message.params.notification.sound).to.eql(data.sound); - expect(message.params.data.sender).to.eql(data.custom.sender); - expect(message.params.priority).to.equal('high'); - // This params are duplicated in order to facilitate extraction - // So they are available as `gcm.notification.title` and as `title` - expect(message.params.data.title).to.eql(data.title); - expect(message.params.data.message).to.eql(data.body); - expect(message.params.data.sound).to.eql(data.sound); - cb(null, { - multicast_id: 'abc', - success: registrationTokens.length, - failure: 0, - results: registrationTokens.map((token) => ({ - message_id: '', - registration_id: token, - error: null, - })), - }); - });