From 5a12a3f10fdb7e2222dc8e125478b0cafcad7495 Mon Sep 17 00:00:00 2001 From: Paul Lessing Date: Mon, 17 Sep 2018 09:06:02 +0100 Subject: [PATCH] Add types for web-push (#28910) --- types/web-push/index.d.ts | 227 +++++++++++++++++++++++ types/web-push/tsconfig.json | 23 +++ types/web-push/tslint.json | 1 + types/web-push/web-push-tests.ts | 303 +++++++++++++++++++++++++++++++ 4 files changed, 554 insertions(+) create mode 100644 types/web-push/index.d.ts create mode 100644 types/web-push/tsconfig.json create mode 100644 types/web-push/tslint.json create mode 100644 types/web-push/web-push-tests.ts diff --git a/types/web-push/index.d.ts b/types/web-push/index.d.ts new file mode 100644 index 0000000000..8e00fbafff --- /dev/null +++ b/types/web-push/index.d.ts @@ -0,0 +1,227 @@ +// Type definitions for web-push 3.3 +// Project: https://github.com/web-push-libs/web-push +// Definitions by: Paul Lessing +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.6 + +/// + +/** + * To send a push notification call this method with a subscription, optional payload and any options. + * + * Note: you don't need to define a payload, and this method will work without a GCM API Key and / or VAPID keys if the push service supports it. + * + * @param subscription The PushSubscription you wish to send the notification to. + * @param payload The payload you wish to send to the the user. + * @param options Options for the GCM API key and vapid keys can be passed in if they are unique for each notification you wish to send. + * + * @throws WebPushError if the upstream push endpoint returns a non-200 status code. + * @returns This method returns a Promise which resolves if the sending of the notification was successful, otherwise it rejects. + * + * @see SendResult + * @see WebPushError + * @see https://github.com/web-push-libs/web-push#sendnotificationpushsubscription-payload-options + */ +export function sendNotification(subscription: PushSubscription, payload?: string | Buffer | null, options?: RequestOptions): Promise; + +/** + * Generate VAPID keys. + * + * @returns Returns an object with publicKey and privateKey values which are URL Safe Base64 encoded strings. + * + * Note: You should create these keys once, store them and use them for all future messages you send. + */ +export function generateVAPIDKeys(): VapidKeys; + +/** + * This method expects the GCM API key that is linked to the gcm_sender_id in your web app manifest. + * You can use a GCM API Key from the Google Developer Console or the Cloud Messaging tab under a Firebase Project. + * + * @param apiKey An API key, or null. + */ +export function setGCMAPIKey(apiKey: string | null): void; + +/** + * Encrypts the payload according to the Message Encryption for Web Push standard. + * + * (sendNotification will automatically encrypt the payload for you, so if you use sendNotification() you don't need to worry about it). + * + * @param userPublicKey The public key of the receiver (from the browser). + * @param userAuth The auth secret of the receiver (from the browser). + * @param payload The message to attach to the notification. + * @param contentEncoding The type of content encoding to use (e.g. aesgcm or aes128gcm). + * + * @returns This method returns an object with the following fields: + * localPublicKey: The public key matched the private key used during encryption. + * salt: A string representing the salt used to encrypt the payload. + * cipherText: The encrypted payload as a Buffer. + * + * @see ContentEncoding + * @see https://github.com/web-push-libs/web-push#encryptuserpublickey-userauth-payload-contentencoding + */ +export function encrypt(userPublicKey: string, userAuth: string, payload: string | Buffer, contentEncoding: ContentEncoding): EncryptionResult; + +/** + * This method takes the required VAPID parameters and returns the required + * header to be added to a Web Push Protocol Request. + * @param audience This must be the origin of the push service. + * @param subject This should be a URL or a 'mailto:' email address. + * @param publicKey The VAPID public key. + * @param privateKey The VAPID private key. + * @param contentEncoding The contentEncoding type. + * @param [expiration] The expiration of the VAPID JWT. + * @returns Returns an Object with the Authorization and 'Crypto-Key' values to be used as headers. + */ +export function getVapidHeaders( + audience: string, subject: string, publicKey: string, privateKey: string, contentEncoding: 'aes128gcm', expiration?: number +): { + Authorization: string; +}; +export function getVapidHeaders( + audience: string, subject: string, publicKey: string, privateKey: string, contentEncoding: 'aesgcm', expiration?: number +): { + Authorization: string; + 'Crypto-Key': string; +}; +export function getVapidHeaders( + audience: string, subject: string, publicKey: string, privateKey: string, contentEncoding: ContentEncoding, expiration?: number +): { + Authorization: string; + 'Crypto-Key'?: string; +}; + +/** + * Note: When calling generateRequestDetails() the payload argument does not need to be defined, passing in null + * will return no body and exclude any unnecessary headers. + * Headers related to the GCM API Key and / or VAPID keys will be included if supplied and required. + * + * @param subscription must be an object containing the details for a push subscription. + * The expected format is the same output as JSON.stringify'ing a PushSubscription in the browser. + * @param payload The payload is optional, but if set, will be encrypted and a Buffer will be returned via the payload parameter. + * This argument must be either a string or a node Buffer. + * Note: In order to encrypt the payload, the pushSubscription must have a keys object with p256dh and auth values. + * @param options Optional argument, none of its parameters are required. + * + * @returns An object containing all the details needed to make a network request. + * + * @see PushSubscription + * @see RequestDetails + * @see https://github.com/web-push-libs/web-push#generaterequestdetailspushsubscription-payload-options + */ +export function generateRequestDetails(subscription: PushSubscription, payload?: null, options?: RequestOptions): RequestDetails & { body: null }; +export function generateRequestDetails(subscription: PushSubscription, payload?: string | Buffer, options?: RequestOptions): RequestDetails & { body: Buffer }; +export function generateRequestDetails(subscription: PushSubscription, payload?: string | Buffer, options?: RequestOptions): RequestDetails; + +/** + * Valid content encodings used by encrypt(), getVapidHeaders(), generateRequestDetails() and sendNotification(). + */ +export type ContentEncoding = 'aesgcm' | 'aes128gcm'; + +/** + * Map of valid content encodings. + */ +export const supportedContentEncodings: { + readonly AES_GCM: 'aesgcm' & ContentEncoding; + readonly AES_128_GCM: 'aws128gcm' & ContentEncoding; +}; + +/** + * Returned by encrypt() on success. + */ +export interface EncryptionResult { + localPublicKey: string; + salt: string; + cipherText: Buffer; +} + +/** + * A private/public key pair for use with VAPID methods. + */ +export interface VapidKeys { + publicKey: string; + privateKey: string; +} + +/** + * When making requests where you want to define VAPID details, call this method before sendNotification() + * or pass in the details and options to sendNotification. + * + * @param subject This must be either a URL or a 'mailto:' address. + * For example: 'https://my-site.com/contact' or 'mailto: contact@my-site.com' + * @param publicKey The public VAPID key. + * @param privateKey The private VAPID key. + */ +export function setVapidDetails(subject: string, publicKey: string, privateKey: string): void; + +/** + * Configuration for a Push Subscription. This can be obtained on the frontend by calling + * serviceWorkerRegistration.pushManager.subscribe(). + * The expected format is the same output as JSON.stringify'ing a PushSubscription in the browser. + */ +export interface PushSubscription { + endpoint: string; + keys: { + p256dh: string; + auth: string; + }; +} + +export interface Headers { + [header: string]: string; +} + +/** + * Options for configuring the outgoing request in generateRequestDetails() or sendNotification(). + */ +export interface RequestOptions { + headers?: Headers; + gcmAPIKey?: string; // can be a GCM API key to be used for this request and this request only. This overrides any API key set via setGCMAPIKey(). + vapidDetails?: { // should be an object with subject, publicKey and privateKey values defined. These values should follow the VAPID Spec. (https://tools.ietf.org/html/draft-thomson-webpush-vapid) + subject: string; + publicKey: string; + privateKey: string; + }; + TTL?: number; // a value in seconds that describes how long a push message is retained by the push service (by default, four weeks). + contentEncoding?: ContentEncoding; // the type of push encoding to use (e.g. 'aesgcm', by default, or 'aes128gcm'). + proxy?: string; // proxy hostname/ip and a port to tunnel your requests through (eg. http://< hostname >:< port >). +} + +/** + * An object containing all the details needed to make a network request. + * Buffer is null unless a payload was passed into generateRequestDetails(). + */ +export interface RequestDetails { + method: 'POST'; + headers: Headers; + body: Buffer | null; + endpoint: string; + proxy?: string; +} + +/** + * If calling sendNotification() was successful, this result contains the response from the push endpoint. + */ +export interface SendResult { + statusCode: number; + body: string; + headers: Headers; +} + +/** + * Error thrown when sendNotification() receives a non-200 status code from the push endpoint. + */ +export class WebPushError extends Error { + readonly message: string; + readonly statusCode: number; + readonly headers: Headers; + readonly body: string; + readonly endpoint: string; + + constructor( + message: string, + statusCode: number, + headers: Headers, + body: string, + endpoint: string + ); +} diff --git a/types/web-push/tsconfig.json b/types/web-push/tsconfig.json new file mode 100644 index 0000000000..3cd908695e --- /dev/null +++ b/types/web-push/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "strictFunctionTypes": true, + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "web-push-tests.ts" + ] +} diff --git a/types/web-push/tslint.json b/types/web-push/tslint.json new file mode 100644 index 0000000000..3db14f85ea --- /dev/null +++ b/types/web-push/tslint.json @@ -0,0 +1 @@ +{ "extends": "dtslint/dt.json" } diff --git a/types/web-push/web-push-tests.ts b/types/web-push/web-push-tests.ts new file mode 100644 index 0000000000..a3020b0144 --- /dev/null +++ b/types/web-push/web-push-tests.ts @@ -0,0 +1,303 @@ +import { + WebPushError, + ContentEncoding, + supportedContentEncodings, + encrypt, + EncryptionResult, + getVapidHeaders, + Headers, + generateVAPIDKeys, + VapidKeys, + setGCMAPIKey, + setVapidDetails, + PushSubscription, + RequestOptions, + RequestDetails, + generateRequestDetails, + SendResult, + sendNotification +} from 'web-push'; + +declare const anything: any; + +// ============== +// WebPushError +// ============== + +// $ExpectType WebPushError +const error = new WebPushError('message', 400, {}, 'body', 'endpoint'); + +// WebPushError extends Error +// $ExpectType string | undefined +error.stack; +// $ExpectType string +error.message; +// $ExpectType string +error.name; + +// ================= +// ContentEncoding +// ================= + +// ContentEncoding should only allow named types + +// $ExpectError +const notAValidEncoding: ContentEncoding = 'hello world'; + +// The exported values should match the "enum" type + +const aesGcm: ContentEncoding = supportedContentEncodings.AES_GCM; +const aes128Gcm: ContentEncoding = supportedContentEncodings.AES_128_GCM; + +// =========== +// encrypt() +// =========== + +// $ExpectType EncryptionResult +const encryptionResult = encrypt('publicKey', 'userAuth', 'myPayload', supportedContentEncodings.AES_GCM); + +const buffer: Buffer = anything; +// $ExpectType EncryptionResult +encrypt('publicKey', 'userAuth', buffer, supportedContentEncodings.AES_128_GCM); + +// Only valid encoding should be allowed +// $ExpectError +encrypt('publicKey', 'userAuth', 'myPayload', 'unknownEncoding'); + +// $ExpectType string +encryptionResult.localPublicKey; +// $ExpectType string +encryptionResult.salt; +// $ExpectType Buffer +encryptionResult.cipherText; + +// =================== +// getVapidHeaders() +// =================== + +// $ExpectType { Authorization: string; 'Crypto-Key': string; } +getVapidHeaders('audience', 'subject', 'publicKey', 'privateKey', supportedContentEncodings.AES_GCM, 150); +// $ExpectType { Authorization: string; } +getVapidHeaders('audience', 'subject', 'publicKey', 'privateKey', supportedContentEncodings.AES_128_GCM, 150); + +// expiration is optional +// $ExpectType { Authorization: string; 'Crypto-Key': string; } +getVapidHeaders('audience', 'subject', 'publicKey', 'privateKey', supportedContentEncodings.AES_GCM); +// $ExpectType { Authorization: string; } +getVapidHeaders('audience', 'subject', 'publicKey', 'privateKey', supportedContentEncodings.AES_128_GCM); + +// Buffers are not supported here +// $ExpectError +getVapidHeaders('audience', buffer, 'publicKey', 'privateKey', supportedContentEncodings.AES_128_GCM); +// $ExpectError +getVapidHeaders('audience', 'subject', buffer, 'privateKey', supportedContentEncodings.AES_128_GCM); +// $ExpectError +getVapidHeaders('audience', 'subject', 'publicKey', buffer, supportedContentEncodings.AES_128_GCM); + +// ===================== +// generateVAPIDKeys() +// ===================== + +// $ExpectType VapidKeys +const vapidKeys = generateVAPIDKeys(); + +// $ExpectType string +vapidKeys.publicKey; +// $ExpectType string +vapidKeys.privateKey; + +// ================ +// setGCMAPIKey() +// ================ + +// $ExpectType void +setGCMAPIKey('apiKey'); +// $ExpectType void +setGCMAPIKey(null); + +// =================== +// setVapidDetails() +// =================== + +// $ExpectType void +setVapidDetails('subject', 'privateKey', 'publicKey'); + +// Buffers are not supported here +// $ExpectError +setVapidDetails(buffer, 'publicKey', 'privateKey'); +// $ExpectError +setVapidDetails('subject', buffer, 'privateKey'); +// $ExpectError +setVapidDetails('subject', 'publicKey', buffer); + +// ================== +// PushSubscription +// ================== + +const pushSubscription: PushSubscription = { + endpoint: 'endpointString', + keys: { + p256dh: 'p256dhString', + auth: 'authString', + } +}; + +// ================ +// RequestOptions +// ================ +let requestOptions: RequestOptions; + +// Can be entirely empty +requestOptions = {}; + +requestOptions = { + headers: { + someHeader: 'value' + } +}; + +requestOptions = { + gcmAPIKey: 'key' +}; + +requestOptions = { + vapidDetails: { + privateKey: 'private', + publicKey: 'public', + subject: 'subject' + } +}; + +requestOptions = { + TTL: 100 +}; + +requestOptions = { + contentEncoding: supportedContentEncodings.AES_128_GCM +}; + +requestOptions = { + proxy: 'http://proxy' +}; + +requestOptions = { + headers: { + someHeader: 'value' + }, + gcmAPIKey: 'key', + vapidDetails: { + privateKey: 'private', + publicKey: 'public', + subject: 'subject' + }, + TTL: 100, + contentEncoding: supportedContentEncodings.AES_GCM, + proxy: 'http://proxy' +}; + +// ========================== +// generateRequestDetails() +// ========================== + +// $ExpectType RequestDetails & { body: Buffer; } +generateRequestDetails(pushSubscription, 'payload', requestOptions); +// $ExpectType Buffer +generateRequestDetails(pushSubscription, 'payload', requestOptions).body; + +// Payload can be a Buffer +// $ExpectType Buffer +generateRequestDetails(pushSubscription, buffer).body; +// $ExpectType Buffer +generateRequestDetails(pushSubscription, buffer, requestOptions).body; + +// Payload is optional, then body will be null +// $ExpectType RequestDetails & { body: null; } +generateRequestDetails(pushSubscription); +// $ExpectType null +generateRequestDetails(pushSubscription).body; +// $ExpectType null +generateRequestDetails(pushSubscription, null).body; +// $ExpectType null +generateRequestDetails(pushSubscription, null, requestOptions).body; + +// PushSubscription must have all its values +// $ExpectError +generateRequestDetails({}); +// $ExpectError +generateRequestDetails({ endpoint: 'endpoint' }); +// $ExpectError +generateRequestDetails({ keys: { p256dh: 'p256dh', auth: 'auth' } }); +// $ExpectError +generateRequestDetails({ endpoint: null, keys: { p256dh: 'p256dh', auth: 'auth' } }); +// $ExpectError +generateRequestDetails({ endpoint: 'endpoint', keys: null }); + +generateRequestDetails({ endpoint: 'endpoint', keys: { p256dh: 'p256dh', auth: 'auth' } }); + +// RequestOptions is optional +generateRequestDetails(pushSubscription, 'payload'); +generateRequestDetails(pushSubscription, 'payload', {}); + +// ================ +// RequestDetails +// ================ +const requestDetails = generateRequestDetails(pushSubscription, 'payload'); +// $ExpectType "POST" +requestDetails.method; +// $ExpectType Headers +requestDetails.headers; +// $ExpectType Buffer +requestDetails.body; +// $ExpectType string +requestDetails.endpoint; +// $ExpectType string | undefined +requestDetails.proxy; + +// ==================== +// sendNotification() +// ==================== +// $ExpectType Promise +sendNotification(pushSubscription, 'payload', requestOptions); + +// Buffers are also supported +// $ExpectType Promise +sendNotification(pushSubscription, buffer); +// $ExpectType Promise +sendNotification(pushSubscription, buffer, requestOptions); + +// Payload can be absent or null +// $ExpectType Promise +sendNotification(pushSubscription); +// $ExpectType Promise +sendNotification(pushSubscription, null, requestOptions); + +// PushSubscription must have all its values +// $ExpectError +sendNotification({}, 'payload'); +// $ExpectError +sendNotification({ endpoint: 'endpoint' }, 'payload'); +// $ExpectError +sendNotification({ keys: { p256dh: 'p256dh', auth: 'auth' } }, 'payload'); +// $ExpectError +sendNotification({ endpoint: null, keys: { p256dh: 'p256dh', auth: 'auth' } }, 'payload'); +// $ExpectError +sendNotification({ endpoint: 'endpoint', keys: null }, 'payload'); + +sendNotification({ endpoint: 'endpoint', keys: { p256dh: 'p256dh', auth: 'auth' } }, 'payload'); + +// ============= +// SendResult +// ============= + +const sendResult = sendNotification(pushSubscription, 'payload'); + +sendResult.then((result) => { + // ExpectType number + result.statusCode; + // $ExpectType string + result.body; + // $ExpectType Headers + result.headers; +}); +sendResult.catch((error: WebPushError) => { +});