UNPKG

@ringcentral/sdk

Version:

- [Installation](#installation) - [Getting Started](#getting-started) - [API Calls](#api-calls) - [Advanced SDK Configuration & Polyfills](#advanced-sdk-configuration--polyfills) - [Making telephony calls](#making-telephony-calls) - [Call management using

621 lines (491 loc) 19.2 kB
import EventEmitter from 'events'; import * as qs from 'querystring'; import Auth, {AuthOptions} from './Auth'; import * as Constants from '../core/Constants'; import Cache from '../core/Cache'; import Client, {ApiError} from '../http/Client'; import Externals from '../core/Externals'; declare const screen: any; //FIXME TS Crap const delay = (timeout): Promise<any> => new Promise((resolve, reject) => { setTimeout(() => { resolve(null); }, timeout); }); const getParts = (url, separator) => url.split(separator).reverse()[0]; export enum events { beforeLogin = 'beforeLogin', loginSuccess = 'loginSuccess', loginError = 'loginError', beforeRefresh = 'beforeRefresh', refreshSuccess = 'refreshSuccess', refreshError = 'refreshError', beforeLogout = 'beforeLogout', logoutSuccess = 'logoutSuccess', logoutError = 'logoutError', rateLimitError = 'rateLimitError', } export default class Platform extends EventEmitter { public static _cacheId = 'platform'; public events = events; private _server: string; private _clientId: string; private _clientSecret: string; private _redirectUri: string; private _refreshDelayMs: number; private _clearCacheOnRefreshError: boolean; private _userAgent: string; private _externals: Externals; private _cache: Cache; private _client: Client; private _refreshPromise: Promise<any>; private _auth: Auth; private _tokenEndpoint; private _revokeEndpoint; private _authorizeEndpoint; private _authProxy; private _urlPrefix; public constructor({ server, clientId, clientSecret, redirectUri = '', refreshDelayMs = 100, clearCacheOnRefreshError = true, appName = '', appVersion = '', externals, cache, client, refreshHandicapMs, tokenEndpoint = '/restapi/oauth/token', revokeEndpoint = '/restapi/oauth/revoke', authorizeEndpoint = '/restapi/oauth/authorize', authProxy = false, urlPrefix = '', }: PlatformOptionsConstructor) { super(); this._server = server; this._clientId = clientId; this._clientSecret = clientSecret; this._redirectUri = redirectUri; this._refreshDelayMs = refreshDelayMs; this._clearCacheOnRefreshError = clearCacheOnRefreshError; this._authProxy = authProxy; this._urlPrefix = urlPrefix; this._userAgent = `${appName ? `${appName + (appVersion ? `/${appVersion}` : '')} ` : ''}RCJSSDK/${ Constants.version }`; this._externals = externals; this._cache = cache; this._client = client; this._refreshPromise = null; this._auth = new Auth({ cache: this._cache, cacheId: Platform._cacheId, refreshHandicapMs, }); this._tokenEndpoint = tokenEndpoint; this._revokeEndpoint = revokeEndpoint; this._authorizeEndpoint = authorizeEndpoint; } public on(event: events.beforeLogin, listener: () => void); public on(event: events.loginSuccess, listener: (response: Response) => void); public on(event: events.loginError, listener: (error: ApiError | Error) => void); public on(event: events.beforeRefresh, listener: () => void); public on(event: events.refreshSuccess, listener: (response: Response) => void); public on(event: events.refreshError, listener: (error: ApiError | Error) => void); public on(event: events.beforeLogout, listener: () => void); public on(event: events.logoutSuccess, listener: (response: Response) => void); public on(event: events.logoutError, listener: (error: ApiError | Error) => void); public on(event: events.rateLimitError, listener: (error: ApiError | Error) => void); public on(event: string, listener: (...args) => void) { return super.on(event, listener); } public auth() { return this._auth; } public createUrl(path = '', options: CreateUrlOptions = {}) { let builtUrl = ''; const hasHttp = path.startsWith('http://') || path.startsWith('https://'); if (options.addServer && !hasHttp) builtUrl += this._server; if (this._urlPrefix) builtUrl += this._urlPrefix; builtUrl += path; if (options.addMethod) builtUrl += `${path.includes('?') ? '&' : '?'}_method=${options.addMethod}`; return builtUrl; } public async signUrl(path: string) { return `${path + (path.includes('?') ? '&' : '?')}access_token=${(await this._auth.data()).access_token}`; } public loginUrl({implicit, state, brandId, display, prompt, uiOptions, uiLocales, localeId}: LoginUrlOptions = {}) { return this.createUrl( `${this._authorizeEndpoint}?${qs.stringify({ response_type: implicit ? 'token' : 'code', redirect_uri: this._redirectUri, client_id: this._clientId, state, brand_id: brandId, display, prompt, ui_options: uiOptions, ui_locales: uiLocales, localeId, })}`, {addServer: true}, ); } /** * @param {string} url * @return {Object} */ public parseLoginRedirect(url: string) { const response = (url.startsWith('#') && getParts(url, '#')) || (url.startsWith('?') && getParts(url, '?')) || null; if (!response) throw new Error('Unable to parse response'); const queryString = qs.parse(response); if (!queryString) throw new Error('Unable to parse response'); const error = queryString.error_description || queryString.error; if (error) { const e: any = new Error(error.toString()); e.error = queryString.error; throw e; } return queryString; } /** * Convenience method to handle 3-legged OAuth * * Attention! This is an experimental method and it's signature and behavior may change without notice. */ public loginWindow({ url, width = 400, height = 600, origin = window.location.origin, property = Constants.authResponseProperty, target = '_blank', }: LoginWindowOptions): Promise<LoginOptions> { return new Promise((resolve, reject) => { if (typeof window === 'undefined') throw new Error('This method can be used only in browser'); if (!url) throw new Error('Missing mandatory URL parameter'); const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : 0; const dualScreenTop = window.screenTop !== undefined ? window.screenTop : 0; const screenWidth = screen.width; const screenHeight = screen.height; const left = screenWidth / 2 - width / 2 + dualScreenLeft; const top = screenHeight / 2 - height / 2 + dualScreenTop; const win = window.open( url, '_blank', target === '_blank' ? `scrollbars=yes, status=yes, width=${width}, height=${height}, left=${left}, top=${top}` : '', ); if (!win) { throw new Error('Could not open login window. Please allow popups for this site'); } if (win.focus) win.focus(); const eventListener = e => { try { if (e.origin !== origin) return; if (!e.data || !e.data[property]) return; // keep waiting win.close(); window.addEventListener('message', eventListener); const loginOptions = this.parseLoginRedirect(e.data[property]); if (!loginOptions.code && !loginOptions.access_token) throw new Error('No authorization code or token'); resolve(loginOptions); } catch (e) { reject(e); } }; window.addEventListener('message', eventListener, false); }); } /** * @return {Promise<boolean>} */ public async loggedIn() { try { if (this._authProxy) { await this.get('/restapi/v1.0/client-info'); // we only can determine the status if we actually make request } else { await this.ensureLoggedIn(); } return true; } catch (e) { return false; } } public async login({ username, password, extension = '', code, access_token_ttl, refresh_token_ttl, access_token, endpoint_id, ...options }: LoginOptions = {}): Promise<Response> { try { this.emit(this.events.beforeLogin); const body: any = {}; let response = null; let json; if (access_token) { //TODO Potentially make a request to /oauth/tokeninfo json = {access_token, ...options}; } else { if (!code) { body.grant_type = 'password'; body.username = username; body.password = password; body.extension = extension; } else if (code) { //@see https://developers.ringcentral.com/legacy-api-reference/index.html#!#RefAuthorizationCodeFlow body.grant_type = 'authorization_code'; body.code = code; body.redirect_uri = this._redirectUri; } if (access_token_ttl) body.access_token_ttl = access_token_ttl; if (refresh_token_ttl) body.refresh_token_ttl = refresh_token_ttl; if (endpoint_id) body.endpoint_id = endpoint_id; response = await this._tokenRequest(this._tokenEndpoint, body); json = await response.clone().json(); } await this._auth.setData(json); this.emit(this.events.loginSuccess, response); return response; } catch (e) { if (this._clearCacheOnRefreshError) await this._cache.clean(); this.emit(this.events.loginError, e); throw e; } } private async _refresh(): Promise<Response> { try { this.emit(this.events.beforeRefresh); await delay(this._refreshDelayMs); const authData = await this.auth().data(); // Perform sanity checks if (!authData.refresh_token) throw new Error('Refresh token is missing'); if (!this._auth.refreshTokenValid()) throw new Error('Refresh token has expired'); const res = await this._tokenRequest(this._tokenEndpoint, { grant_type: 'refresh_token', refresh_token: authData.refresh_token, access_token_ttl: authData.expires_in + 1, refresh_token_ttl: authData.refresh_token_expires_in + 1, }); const json = await res.clone().json(); if (!json.access_token) { throw await this._client.makeError(new Error('Malformed OAuth response'), res); } await this._auth.setData(json); this.emit(this.events.refreshSuccess, res); return res; } catch (e) { if (this._clearCacheOnRefreshError) { await this._cache.clean(); } this.emit(this.events.refreshError, e); throw e; } } public async refresh(): Promise<Response> { if (this._authProxy) { throw new Error('Refresh is not supported in Auth Proxy mode'); } if (!this._refreshPromise) { this._refreshPromise = (async () => { try { const res = await this._refresh(); this._refreshPromise = null; return res; } catch (e) { this._refreshPromise = null; throw e; } })(); } return this._refreshPromise; } public async logout(): Promise<Response> { if (this._authProxy) { throw new Error('Logout is not supported in Auth Proxy mode'); } try { this.emit(this.events.beforeLogout); let res = null; //FIXME https://developers.ringcentral.com/legacy-api-reference/index.html#!#RefRevokeToken.html requires secret if (this._revokeEndpoint && this._clientSecret) { res = await this._tokenRequest(this._revokeEndpoint, { token: (await this._auth.data()).access_token, }); } await this._cache.clean(); this.emit(this.events.logoutSuccess, res); return res; } catch (e) { this.emit(this.events.logoutError, e); throw e; } } public async inflateRequest(request: Request, options: SendOptions = {}): Promise<Request> { options = options || {}; request.headers.set('X-User-Agent', this._userAgent); if (options.skipAuthCheck) return request; await this.ensureLoggedIn(); request.headers.set('Client-Id', this._clientId); if (!this._authProxy) request.headers.set('Authorization', await this.authHeader()); return request; } public async sendRequest(request: Request, options: SendOptions = {}): Promise<Response> { try { request = await this.inflateRequest(request, options); return await this._client.sendRequest(request); } catch (e) { const {retry, handleRateLimit} = options; // Guard is for errors that come from polling if (!e.response || retry) throw e; const {response} = e; const {status} = response; if ((status !== Client._unauthorizedStatus && status !== Client._rateLimitStatus) || this._authProxy) throw e; options.retry = true; let retryAfter = 0; if (status === Client._unauthorizedStatus) { await this._auth.cancelAccessToken(); } if (status === Client._rateLimitStatus) { const defaultRetryAfter = !handleRateLimit || typeof handleRateLimit === 'boolean' ? 60 : handleRateLimit; // FIXME retry-after is custom header, by default, it can't be retrieved. Server should add header: 'Access-Control-Expose-Headers: retry-after'. retryAfter = parseFloat(response.headers.get('retry-after') || defaultRetryAfter) * 1000; e.retryAfter = retryAfter; this.emit(this.events.rateLimitError, e); if (!options.handleRateLimit) throw e; } await delay(retryAfter); return this.sendRequest(this._client.createRequest(options), options); } } public send(options: SendOptions = {}) { //FIXME https://github.com/bitinn/node-fetch/issues/43 options.url = this.createUrl(options.url, {addServer: true}); return this.sendRequest(this._client.createRequest(options), options); } public async get(url, query?, options?: SendOptions): Promise<Response> { return this.send({method: 'GET', url, query, ...options}); } public async post(url, body?, query?, options?: SendOptions): Promise<Response> { return this.send({method: 'POST', url, query, body, ...options}); } public async put(url, body?, query?, options?: SendOptions): Promise<Response> { return this.send({method: 'PUT', url, query, body, ...options}); } public async delete(url, query?, options?: SendOptions): Promise<Response> { return this.send({method: 'DELETE', url, query, ...options}); } public async ensureLoggedIn(): Promise<Response | null> { if (this._authProxy) return null; if (await this._auth.accessTokenValid()) return null; await this.refresh(); return null; } protected async _tokenRequest(url, body): Promise<Response> { return this.send({ url, body, skipAuthCheck: true, method: 'POST', headers: { Authorization: this.basicAuthHeader(), 'Content-Type': Client._urlencodedContentType, }, }); } public basicAuthHeader(): string { const apiKey = this._clientId + (this._clientSecret ? `:${this._clientSecret}` : ''); return `Basic ${typeof btoa === 'function' ? btoa(apiKey) : Buffer.from(apiKey).toString('base64')}`; } public async authHeader(): Promise<string> { const data = await this._auth.data(); return (data.token_type || 'Bearer') + (data.access_token ? ` ${data.access_token}` : ''); } } export interface PlatformOptions extends AuthOptions { server?: string; clientId?: string; clientSecret?: string; redirectUri?: string; refreshDelayMs?: number; refreshHandicapMs?: number; clearCacheOnRefreshError?: boolean; appName?: string; appVersion?: string; tokenEndpoint?: string; revokeEndpoint?: string; authorizeEndpoint?: string; authProxy?: boolean; urlPrefix?: string; } export interface PlatformOptionsConstructor extends PlatformOptions { externals: Externals; cache: Cache; client: Client; } export interface SendOptions { url?: any; body?: any; method?: string; query?: any; headers?: any; skipAuthCheck?: boolean; handleRateLimit?: boolean | number; retry?: boolean; // Will be set by this method if SDK makes second request } export interface LoginOptions { username?: string; password?: string; extension?: string; code?: string; access_token?: string; access_token_ttl?: number; refresh_token_ttl?: number; endpoint_id?: string; } export interface LoginUrlOptions { state?: string; brandId?: string; display?: LoginUrlDisplay | string; prompt?: LoginUrlPrompt | string; implicit?: boolean; uiOptions?: string; uiLocales?: string; localeId?: string; } export enum LoginUrlPrompt { login = 'login', sso = 'sso', consent = 'consent', none = 'none', } export enum LoginUrlDisplay { page = 'page', popup = 'popup', touch = 'touch', mobile = 'mobile', } export interface CreateUrlOptions { addServer?: boolean; addMethod?: string; } export interface LoginWindowOptions { url: string; width?: number; height?: number; origin?: string; property?: string; target?: string; }