expo-server-sdk
Advanced tools
Comparing version 3.11.0 to 3.12.0
@@ -1,3 +0,2 @@ | ||
/// <reference types="node" /> | ||
import { Agent } from 'http'; | ||
import { Agent } from 'node:http'; | ||
export declare class Expo { | ||
@@ -10,3 +9,4 @@ static pushNotificationChunkSizeLimit: number; | ||
private useFcmV1; | ||
constructor(options?: ExpoClientOptions); | ||
private retryMinTimeout; | ||
constructor(options?: Partial<ExpoClientOptions>); | ||
/** | ||
@@ -50,6 +50,7 @@ * Returns `true` if the token is an Expo push token | ||
export type ExpoClientOptions = { | ||
httpAgent?: Agent; | ||
maxConcurrentRequests?: number; | ||
accessToken?: string; | ||
useFcmV1?: boolean; | ||
httpAgent: Agent; | ||
maxConcurrentRequests: number; | ||
retryMinTimeout: number; | ||
accessToken: string; | ||
useFcmV1: boolean; | ||
}; | ||
@@ -59,3 +60,3 @@ export type ExpoPushToken = string; | ||
to: ExpoPushToken | ExpoPushToken[]; | ||
data?: object; | ||
data?: Record<string, unknown>; | ||
title?: string; | ||
@@ -76,2 +77,3 @@ subtitle?: string; | ||
mutableContent?: boolean; | ||
_contentAvailable?: boolean; | ||
}; | ||
@@ -95,6 +97,6 @@ export type ExpoPushReceiptId = string; | ||
error?: 'DeveloperError' | 'DeviceNotRegistered' | 'ExpoError' | 'InvalidCredentials' | 'MessageRateExceeded' | 'MessageTooBig' | 'ProviderError'; | ||
expoPushToken?: string; | ||
}; | ||
expoPushToken?: string; | ||
__debug?: any; | ||
}; | ||
export type ExpoPushReceipt = ExpoPushSuccessReceipt | ExpoPushErrorReceipt; |
@@ -25,11 +25,2 @@ "use strict"; | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -47,14 +38,20 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
*/ | ||
const assert_1 = __importDefault(require("assert")); | ||
const node_fetch_1 = __importStar(require("node-fetch")); | ||
const node_assert_1 = __importDefault(require("node:assert")); | ||
const node_zlib_1 = require("node:zlib"); | ||
const promise_limit_1 = __importDefault(require("promise-limit")); | ||
const promise_retry_1 = __importDefault(require("promise-retry")); | ||
const zlib_1 = __importDefault(require("zlib")); | ||
const ExpoClientValues_1 = require("./ExpoClientValues"); | ||
class Expo { | ||
static pushNotificationChunkSizeLimit = ExpoClientValues_1.pushNotificationChunkLimit; | ||
static pushNotificationReceiptChunkSizeLimit = ExpoClientValues_1.pushNotificationReceiptChunkLimit; | ||
httpAgent; | ||
limitConcurrentRequests; | ||
accessToken; | ||
useFcmV1; | ||
retryMinTimeout; | ||
constructor(options = {}) { | ||
this.httpAgent = options.httpAgent; | ||
this.limitConcurrentRequests = (0, promise_limit_1.default)(options.maxConcurrentRequests != null | ||
? options.maxConcurrentRequests | ||
: ExpoClientValues_1.defaultConcurrentRequestLimit); | ||
this.limitConcurrentRequests = (0, promise_limit_1.default)(options.maxConcurrentRequests ?? ExpoClientValues_1.defaultConcurrentRequestLimit); | ||
this.retryMinTimeout = options.retryMinTimeout ?? ExpoClientValues_1.requestRetryMinTimeout; | ||
this.accessToken = options.accessToken; | ||
@@ -83,58 +80,54 @@ this.useFcmV1 = options.useFcmV1; | ||
*/ | ||
sendPushNotificationsAsync(messages) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const url = new URL(ExpoClientValues_1.sendApiUrl); | ||
// Only append the useFcmV1 option if the option is set to false | ||
if (this.useFcmV1 === false) { | ||
url.searchParams.append('useFcmV1', String(this.useFcmV1)); | ||
} | ||
const actualMessagesCount = Expo._getActualMessageCount(messages); | ||
const data = yield this.limitConcurrentRequests(() => __awaiter(this, void 0, void 0, function* () { | ||
return yield (0, promise_retry_1.default)((retry) => __awaiter(this, void 0, void 0, function* () { | ||
try { | ||
return yield this.requestAsync(url.toString(), { | ||
httpMethod: 'post', | ||
body: messages, | ||
shouldCompress(body) { | ||
return body.length > 1024; | ||
}, | ||
}); | ||
async sendPushNotificationsAsync(messages) { | ||
const url = new URL(ExpoClientValues_1.sendApiUrl); | ||
// Only append the useFcmV1 option if the option is set to false | ||
if (this.useFcmV1 === false) { | ||
url.searchParams.append('useFcmV1', String(this.useFcmV1)); | ||
} | ||
const actualMessagesCount = Expo._getActualMessageCount(messages); | ||
const data = await this.limitConcurrentRequests(async () => { | ||
return await (0, promise_retry_1.default)(async (retry) => { | ||
try { | ||
return await this.requestAsync(url.toString(), { | ||
httpMethod: 'post', | ||
body: messages, | ||
shouldCompress(body) { | ||
return body.length > 1024; | ||
}, | ||
}); | ||
} | ||
catch (e) { | ||
// if Expo servers rate limit, retry with exponential backoff | ||
if (e.statusCode === 429) { | ||
return retry(e); | ||
} | ||
catch (e) { | ||
// if Expo servers rate limit, retry with exponential backoff | ||
if (e.statusCode === 429) { | ||
return retry(e); | ||
} | ||
throw e; | ||
} | ||
}), { | ||
retries: 2, | ||
factor: 2, | ||
minTimeout: ExpoClientValues_1.requestRetryMinTimeout, | ||
}); | ||
})); | ||
if (!Array.isArray(data) || data.length !== actualMessagesCount) { | ||
const apiError = new Error(`Expected Expo to respond with ${actualMessagesCount} ${actualMessagesCount === 1 ? 'ticket' : 'tickets'} but got ${data.length}`); | ||
apiError.data = data; | ||
throw apiError; | ||
} | ||
return data; | ||
throw e; | ||
} | ||
}, { | ||
retries: 2, | ||
factor: 2, | ||
minTimeout: this.retryMinTimeout, | ||
}); | ||
}); | ||
if (!Array.isArray(data) || data.length !== actualMessagesCount) { | ||
const apiError = new Error(`Expected Expo to respond with ${actualMessagesCount} ${actualMessagesCount === 1 ? 'ticket' : 'tickets'} but got ${data.length}`); | ||
apiError['data'] = data; | ||
throw apiError; | ||
} | ||
return data; | ||
} | ||
getPushNotificationReceiptsAsync(receiptIds) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const data = yield this.requestAsync(ExpoClientValues_1.getReceiptsApiUrl, { | ||
httpMethod: 'post', | ||
body: { ids: receiptIds }, | ||
shouldCompress(body) { | ||
return body.length > 1024; | ||
}, | ||
}); | ||
if (!data || typeof data !== 'object' || Array.isArray(data)) { | ||
const apiError = new Error(`Expected Expo to respond with a map from receipt IDs to receipts but received data of another type`); | ||
apiError.data = data; | ||
throw apiError; | ||
} | ||
return data; | ||
async getPushNotificationReceiptsAsync(receiptIds) { | ||
const data = await this.requestAsync(ExpoClientValues_1.getReceiptsApiUrl, { | ||
httpMethod: 'post', | ||
body: { ids: receiptIds }, | ||
shouldCompress(body) { | ||
return body.length > 1024; | ||
}, | ||
}); | ||
if (!data || typeof data !== 'object' || Array.isArray(data)) { | ||
const apiError = new Error(`Expected Expo to respond with a map from receipt IDs to receipts but received data of another type`); | ||
apiError['data'] = data; | ||
throw apiError; | ||
} | ||
return data; | ||
} | ||
@@ -154,3 +147,3 @@ chunkPushNotifications(messages) { | ||
// Then create a new chunk to continue on the remaining recipients for this message. | ||
chunk.push(Object.assign(Object.assign({}, message), { to: partialTo })); | ||
chunk.push({ ...message, to: partialTo }); | ||
chunks.push(chunk); | ||
@@ -164,3 +157,3 @@ chunk = []; | ||
// Add remaining `partialTo` to the chunk. | ||
chunk.push(Object.assign(Object.assign({}, message), { to: partialTo })); | ||
chunk.push({ ...message, to: partialTo }); | ||
} | ||
@@ -204,79 +197,73 @@ } | ||
} | ||
requestAsync(url, options) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
let requestBody; | ||
const sdkVersion = require('../package.json').version; | ||
const requestHeaders = new node_fetch_1.Headers({ | ||
Accept: 'application/json', | ||
'Accept-Encoding': 'gzip, deflate', | ||
'User-Agent': `expo-server-sdk-node/${sdkVersion}`, | ||
}); | ||
if (this.accessToken) { | ||
requestHeaders.set('Authorization', `Bearer ${this.accessToken}`); | ||
} | ||
if (options.body != null) { | ||
const json = JSON.stringify(options.body); | ||
(0, assert_1.default)(json != null, `JSON request body must not be null`); | ||
if (options.shouldCompress(json)) { | ||
requestBody = yield gzipAsync(Buffer.from(json)); | ||
requestHeaders.set('Content-Encoding', 'gzip'); | ||
} | ||
else { | ||
requestBody = json; | ||
} | ||
requestHeaders.set('Content-Type', 'application/json'); | ||
} | ||
const response = yield (0, node_fetch_1.default)(url, { | ||
method: options.httpMethod, | ||
body: requestBody, | ||
headers: requestHeaders, | ||
agent: this.httpAgent, | ||
}); | ||
if (response.status !== 200) { | ||
const apiError = yield this.parseErrorResponseAsync(response); | ||
throw apiError; | ||
} | ||
const textBody = yield response.text(); | ||
// We expect the API response body to be JSON | ||
let result; | ||
try { | ||
result = JSON.parse(textBody); | ||
} | ||
catch (_a) { | ||
const apiError = yield this.getTextResponseErrorAsync(response, textBody); | ||
throw apiError; | ||
} | ||
if (result.errors) { | ||
const apiError = this.getErrorFromResult(response, result); | ||
throw apiError; | ||
} | ||
return result.data; | ||
async requestAsync(url, options) { | ||
let requestBody; | ||
const sdkVersion = require('../package.json').version; | ||
const requestHeaders = new node_fetch_1.Headers({ | ||
Accept: 'application/json', | ||
'Accept-Encoding': 'gzip, deflate', | ||
'User-Agent': `expo-server-sdk-node/${sdkVersion}`, | ||
}); | ||
} | ||
parseErrorResponseAsync(response) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const textBody = yield response.text(); | ||
let result; | ||
try { | ||
result = JSON.parse(textBody); | ||
if (this.accessToken) { | ||
requestHeaders.set('Authorization', `Bearer ${this.accessToken}`); | ||
} | ||
if (options.body != null) { | ||
const json = JSON.stringify(options.body); | ||
(0, node_assert_1.default)(json != null, `JSON request body must not be null`); | ||
if (options.shouldCompress(json)) { | ||
requestBody = (0, node_zlib_1.gzipSync)(Buffer.from(json)); | ||
requestHeaders.set('Content-Encoding', 'gzip'); | ||
} | ||
catch (_a) { | ||
return yield this.getTextResponseErrorAsync(response, textBody); | ||
else { | ||
requestBody = json; | ||
} | ||
if (!result.errors || !Array.isArray(result.errors) || !result.errors.length) { | ||
const apiError = yield this.getTextResponseErrorAsync(response, textBody); | ||
apiError.errorData = result; | ||
return apiError; | ||
} | ||
return this.getErrorFromResult(response, result); | ||
requestHeaders.set('Content-Type', 'application/json'); | ||
} | ||
const response = await (0, node_fetch_1.default)(url, { | ||
method: options.httpMethod, | ||
body: requestBody, | ||
headers: requestHeaders, | ||
agent: this.httpAgent, | ||
}); | ||
if (response.status !== 200) { | ||
const apiError = await this.parseErrorResponseAsync(response); | ||
throw apiError; | ||
} | ||
const textBody = await response.text(); | ||
// We expect the API response body to be JSON | ||
let result; | ||
try { | ||
result = JSON.parse(textBody); | ||
} | ||
catch { | ||
const apiError = await this.getTextResponseErrorAsync(response, textBody); | ||
throw apiError; | ||
} | ||
if (result.errors) { | ||
const apiError = this.getErrorFromResult(response, result); | ||
throw apiError; | ||
} | ||
return result.data; | ||
} | ||
getTextResponseErrorAsync(response, text) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const apiError = new Error(`Expo responded with an error with status code ${response.status}: ` + text); | ||
apiError.statusCode = response.status; | ||
apiError.errorText = text; | ||
async parseErrorResponseAsync(response) { | ||
const textBody = await response.text(); | ||
let result; | ||
try { | ||
result = JSON.parse(textBody); | ||
} | ||
catch { | ||
return await this.getTextResponseErrorAsync(response, textBody); | ||
} | ||
if (!result.errors || !Array.isArray(result.errors) || !result.errors.length) { | ||
const apiError = await this.getTextResponseErrorAsync(response, textBody); | ||
apiError['errorData'] = result; | ||
return apiError; | ||
}); | ||
} | ||
return this.getErrorFromResult(response, result); | ||
} | ||
async getTextResponseErrorAsync(response, text) { | ||
const apiError = new Error(`Expo responded with an error with status code ${response.status}: ` + text); | ||
apiError['statusCode'] = response.status; | ||
apiError['errorText'] = text; | ||
return apiError; | ||
} | ||
/** | ||
@@ -287,9 +274,11 @@ * Returns an error for the first API error in the result, with an optional `others` field that | ||
getErrorFromResult(response, result) { | ||
(0, assert_1.default)(result.errors && result.errors.length > 0, `Expected at least one error from Expo`); | ||
const noErrorsMessage = `Expected at least one error from Expo`; | ||
(0, node_assert_1.default)(result.errors, noErrorsMessage); | ||
const [errorData, ...otherErrorData] = result.errors; | ||
node_assert_1.default.ok(errorData, noErrorsMessage); | ||
const error = this.getErrorFromResultError(errorData); | ||
if (otherErrorData.length) { | ||
error.others = otherErrorData.map((data) => this.getErrorFromResultError(data)); | ||
error['others'] = otherErrorData.map((data) => this.getErrorFromResultError(data)); | ||
} | ||
error.statusCode = response.status; | ||
error['statusCode'] = response.status; | ||
return error; | ||
@@ -302,8 +291,8 @@ } | ||
const error = new Error(errorData.message); | ||
error.code = errorData.code; | ||
error['code'] = errorData.code; | ||
if (errorData.details != null) { | ||
error.details = errorData.details; | ||
error['details'] = errorData.details; | ||
} | ||
if (errorData.stack != null) { | ||
error.serverStack = errorData.stack; | ||
error['serverStack'] = errorData.stack; | ||
} | ||
@@ -325,19 +314,5 @@ return error; | ||
exports.Expo = Expo; | ||
Expo.pushNotificationChunkSizeLimit = ExpoClientValues_1.pushNotificationChunkLimit; | ||
Expo.pushNotificationReceiptChunkSizeLimit = ExpoClientValues_1.pushNotificationReceiptChunkLimit; | ||
exports.default = Expo; | ||
function gzipAsync(data) { | ||
return new Promise((resolve, reject) => { | ||
zlib_1.default.gzip(data, (error, result) => { | ||
if (error) { | ||
reject(error); | ||
} | ||
else { | ||
resolve(result); | ||
} | ||
}); | ||
}); | ||
} | ||
class ExtensibleError extends Error { | ||
} | ||
//# sourceMappingURL=ExpoClient.js.map |
"use strict"; | ||
var _a; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -11,3 +10,3 @@ exports.requestRetryMinTimeout = exports.defaultConcurrentRequestLimit = exports.pushNotificationReceiptChunkLimit = exports.pushNotificationChunkLimit = exports.getReceiptsApiUrl = exports.sendApiUrl = void 0; | ||
*/ | ||
const baseUrl = (_a = process.env.EXPO_BASE_URL) !== null && _a !== void 0 ? _a : 'https://exp.host'; | ||
const baseUrl = process.env['EXPO_BASE_URL'] ?? 'https://exp.host'; | ||
exports.sendApiUrl = `${baseUrl}/--/api/v2/push/send`; | ||
@@ -14,0 +13,0 @@ exports.getReceiptsApiUrl = `${baseUrl}/--/api/v2/push/getReceipts`; |
{ | ||
"name": "expo-server-sdk", | ||
"version": "3.11.0", | ||
"version": "3.12.0", | ||
"description": "Server-side library for working with Expo using Node.js", | ||
@@ -11,3 +11,3 @@ "main": "build/ExpoClient.js", | ||
"scripts": { | ||
"build": "./build.sh", | ||
"build": "tsc --project tsconfig.build.json", | ||
"lint": "eslint src", | ||
@@ -24,2 +24,10 @@ "prepare": "yarn build", | ||
"coverageDirectory": "<rootDir>/../coverage", | ||
"coverageThreshold": { | ||
"global": { | ||
"branches": 100, | ||
"functions": 100, | ||
"lines": 100, | ||
"statements": 0 | ||
} | ||
}, | ||
"preset": "ts-jest", | ||
@@ -49,13 +57,15 @@ "rootDir": "src", | ||
"devDependencies": { | ||
"@types/jest": "^29.5.12", | ||
"@tsconfig/node-lts": "^20.1.3", | ||
"@tsconfig/strictest": "^2.0.5", | ||
"@types/node-fetch": "^2.6.11", | ||
"@types/promise-retry": "^1.1.6", | ||
"eslint": "^8.57.0", | ||
"eslint-config-universe": "^12.0.0", | ||
"eslint-config-universe": "^13.0.0", | ||
"fetch-mock": "^9.11.0", | ||
"jest": "^29.7.0", | ||
"prettier": "^3.2.5", | ||
"ts-jest": "~29.1.2", | ||
"ts-jest": "~29.2.5", | ||
"typescript": "^5.4.2" | ||
} | ||
}, | ||
"packageManager": "yarn@4.4.1" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
0
35815
11
455