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 mana

1,005 lines (858 loc) 34 kB
import { createHash, randomBytes, } from 'crypto'; import {EventEmitter} from 'events'; import Cache from '../core/Cache'; import * as Constants from '../core/Constants'; import Externals from '../core/Externals'; import Client, {ApiError} from '../http/Client'; import {objectToUrlParams} from '../http/utils'; import Auth, {AuthOptions} from './Auth'; import Discovery from './Discovery'; import {delay} from './utils'; declare const screen: any; //FIXME TS Crap 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', } function checkPathHasHttp(path) { return path.startsWith('http://') || path.startsWith('https://'); } export default class Platform extends EventEmitter { public static _cacheId = 'platform'; public static _discoveryCacheId = 'platform-discovery'; public events = events; private _server: string; private _rcvServer: string; private _clientId: string; private _clientSecret: string; private _redirectUri: string; private _brandId: 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; private _handleRateLimit: boolean | number; private _codeVerifier: string; private _discovery?: Discovery; private _discoveryInitPromise: Promise<void>; private _loginWindowCheckTimeout?: ReturnType<typeof setTimeout>; private _loginWindowEventListener?: (event: MessageEvent) => void; public constructor({ server, clientId, clientSecret, brandId, redirectUri = '', refreshDelayMs = 100, clearCacheOnRefreshError = true, appName = '', appVersion = '', additionalUserAgent = '', externals, cache, client, refreshHandicapMs, tokenEndpoint = '/restapi/oauth/token', revokeEndpoint = '/restapi/oauth/revoke', authorizeEndpoint = '/restapi/oauth/authorize', enableDiscovery = false, discoveryServer, discoveryInitialEndpoint = '/.well-known/entry-points/initial', discoveryAutoInit = true, authProxy = false, urlPrefix = '', handleRateLimit, }: PlatformOptionsConstructor) { super(); this._server = server; this._rcvServer = server; this._clientId = clientId; this._clientSecret = clientSecret; this._brandId = brandId; this._redirectUri = redirectUri; this._refreshDelayMs = refreshDelayMs; this._clearCacheOnRefreshError = clearCacheOnRefreshError; this._authProxy = authProxy; this._urlPrefix = urlPrefix; this._userAgent = `${appName ? `${appName + (appVersion ? `/${appVersion}` : '')} ` : ''}RCJSSDK/${ Constants.version }${additionalUserAgent ? ` ${additionalUserAgent}` : ''}`; 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; this._handleRateLimit = handleRateLimit; this._codeVerifier = ''; if (enableDiscovery) { const initialEndpoint = discoveryServer ? `${discoveryServer}${discoveryInitialEndpoint}` : discoveryInitialEndpoint; this._discovery = new Discovery({ clientId, brandId, cache: this._cache, cacheId: Platform._discoveryCacheId, initialEndpoint, fetchGet: this.get.bind(this), }); this._discovery.on(this._discovery.events.initialized, discoveryData => { this._authorizeEndpoint = discoveryData.authApi.authorizationUri; }); this._client.on(this._client.events.requestSuccess, response => { if (response.headers.get('discovery-required')) { this._discovery.refreshExternalData(); } }); if (discoveryAutoInit) { this._discoveryInitPromise = this.initDiscovery(); } } } 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 discovery() { return this._discovery; } public createUrl(path = '', options: CreateUrlOptions = {}) { let builtUrl = ''; const hasHttp = checkPathHasHttp(path); if (options.addServer && !hasHttp) { if (path.indexOf('/rcvideo') === 0 || (this._urlPrefix && this._urlPrefix.indexOf('/rcvideo') === 0)) { builtUrl += this._rcvServer; } else { 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 async loginUrlWithDiscovery(options: LoginUrlOptions = {}) { if (this._discovery) { try { // fetch new discovery when generate login url const discoveryData = await this._discovery.fetchInitialData(); this._authorizeEndpoint = discoveryData.authApi.authorizationUri; if (this._discoveryInitPromise) { // await init discovery if it's not initialized await this._discoveryInitPromise; } } catch (e) { const discoveryData = await this._discovery.initialData(); if (!discoveryData) { throw e; } // feedback to use the cached data this._authorizeEndpoint = discoveryData.authApi.authorizationUri; } } return this.loginUrl(options); } public async initDiscovery() { if (!this._discovery) { throw new Error('Discovery is not enabled!'); } try { await this._discovery.init(); this._discoveryInitPromise = null; } catch (e) { this._discoveryInitPromise = null; throw e; } } public loginUrl({ implicit, state, brandId, display, prompt, uiOptions, uiLocales, localeId, usePKCE, responseHint, redirectUri, loginHint, }: LoginUrlOptions = {}) { const query: AuthorizationQuery = { response_type: implicit ? 'token' : 'code', redirect_uri: redirectUri ? redirectUri : this._redirectUri, client_id: this._clientId, state, brand_id: brandId ? brandId : this._brandId, display, prompt, ui_options: uiOptions, ui_locales: uiLocales, localeId, }; if (responseHint) { query.response_hint = responseHint; } if (!!loginHint) { query.login_hint = loginHint; } if (this._discovery) { if (!this._discovery.initialized) { throw new Error('Discovery is not initialized'); } query.discovery = true; } if (usePKCE && implicit) { throw new Error('PKCE only works with Authorization Code Flow'); } this._codeVerifier = ''; if (usePKCE) { this._codeVerifier = this._generateCodeVerifier(); query.code_challenge = createHash('sha256') .update(this._codeVerifier) .digest() .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); query.code_challenge_method = 'S256'; } return this.createUrl(`${this._authorizeEndpoint}?${objectToUrlParams(query)}`, {addServer: true}); } /** * @return {string} */ private _generateCodeVerifier() { let codeVerifier: any = randomBytes(32); codeVerifier = codeVerifier .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); return codeVerifier; } /** * @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 = new URLSearchParams(response); if (!queryString) {throw new Error('Unable to parse response');} const error = queryString.get('error_description') || queryString.get('error'); if (error) { const e: any = new Error(error.toString()); e.error = queryString.get('error'); throw e; } return Object.fromEntries(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 = 'RingCentralLoginWindow', }: LoginWindowOptions): Promise<LoginOptions> { // clear check last timeout when user open loginWindow twice to avoid leak this._clearLoginWindowCheckTimeout(); 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, target, 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();} // clear listener when user open loginWindow twice to avoid leak if (this._loginWindowEventListener) { window.removeEventListener('message', this._loginWindowEventListener); } this._loginWindowEventListener = e => { try { if (e.origin !== origin) {return;} if (!e.data || !e.data[property]) {return;} // keep waiting this._clearLoginWindowCheckTimeout(); win.close(); window.removeEventListener('message', this._loginWindowEventListener); this._loginWindowEventListener = null; const loginOptions = this.parseLoginRedirect(e.data[property]); if (!loginOptions.code && !loginOptions.access_token) {throw new Error('No authorization code or token');} resolve(loginOptions); } catch (e1) { reject(e1); } }; window.addEventListener('message', this._loginWindowEventListener, false); this._createLoginWindowCheckTimeout(win, reject); }); } private _createLoginWindowCheckTimeout(win, reject) { this._loginWindowCheckTimeout = setTimeout(() => { if (win.closed) { if (this._loginWindowEventListener) { window.removeEventListener('message', this._loginWindowEventListener); this._loginWindowEventListener = null; } this._loginWindowCheckTimeout = null; reject(new Error('Login window is closed')); return; } this._createLoginWindowCheckTimeout(win, reject); }, 2000); } private _clearLoginWindowCheckTimeout() { if (this._loginWindowCheckTimeout) { clearTimeout(this._loginWindowCheckTimeout); this._loginWindowCheckTimeout = null; } } /** * @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, jwt, access_token_ttl, refresh_token_ttl, access_token, endpoint_id, token_uri, discovery_uri, code_verifier, redirect_uri, ...options }: LoginOptions = {}): Promise<Response> { try { this.emit(this.events.beforeLogin); const body: any = {}; let response = null; let json; let tokenEndpoint = this._tokenEndpoint; let discoveryEndpoint; if (this._discovery) { const discovery = await this._getTokenAndDiscoveryUriOnLogin({token_uri, discovery_uri}); tokenEndpoint = discovery.tokenEndpoint; discoveryEndpoint = discovery.discoveryEndpoint; } const codeVerifier = code_verifier ? code_verifier : this._codeVerifier; if (access_token) { //TODO Potentially make a request to /oauth/tokeninfo json = {access_token, ...options}; } else { if (!code && !jwt) { body.grant_type = 'password'; if (extension && extension.length > 0) { body.username = `${username}*${extension}`; } else { body.username = username; } body.password = password; // eslint-disable-next-line no-console console.warn( 'Username/password authentication is deprecated. Please migrate to the JWT grant type.', ); } else if (jwt) { body.grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; body.assertion = jwt; } 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 = redirect_uri ? redirect_uri : this._redirectUri; if (codeVerifier && codeVerifier.length > 0) { body.code_verifier = codeVerifier; } } 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(tokenEndpoint, body); json = await response.clone().json(); } await this._auth.setData({ ...json, code_verifier: codeVerifier, }); if (discoveryEndpoint) { await this._discovery.fetchExternalData(discoveryEndpoint); } this.emit(this.events.loginSuccess, response); return response; } catch (e) { if (this._clearCacheOnRefreshError) { await this._cache.clean(); if (this._discovery) { this._discoveryInitPromise = this.initDiscovery(); // request new init data after refresh error and cache cleaned } } this.emit(this.events.loginError, e); throw e; } } private async _getTokenAndDiscoveryUriOnLogin({token_uri, discovery_uri}) { let tokenEndpoint = token_uri; let discoveryEndpoint = discovery_uri; if (tokenEndpoint && discoveryEndpoint) { return {tokenEndpoint, discoveryEndpoint}; } // wait discovery initial finished if (this._discoveryInitPromise) { await this._discoveryInitPromise; } let discoveryData = await this._discovery.initialData(); // check if discovery data is initialized successfully if (!discoveryData) { // discovery request fail in previous init, try re-fetch discovery discoveryData = await this._discovery.fetchInitialData(); } if (!tokenEndpoint) { tokenEndpoint = discoveryData.authApi.defaultTokenUri; } if (!discoveryEndpoint) { discoveryEndpoint = discoveryData.discoveryApi.defaultExternalUri; } return {tokenEndpoint, discoveryEndpoint}; } 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');} const refreshTokenValid = await this._auth.refreshTokenValid(); if (!refreshTokenValid) {throw new Error('Refresh token has expired');} const body: RefreshTokenBody = { grant_type: 'refresh_token', refresh_token: authData.refresh_token, access_token_ttl: parseInt(authData.expires_in), refresh_token_ttl: parseInt(authData.refresh_token_expires_in), }; let tokenEndpoint = this._tokenEndpoint; if (this._discovery) { const discoveryData = await this._discovery.externalData(); if (discoveryData) { tokenEndpoint = discoveryData.authApi.tokenUri; } else { // For user who logged before discovery enabled. Need to refresh token firstly, then get discovery data if (this._discoveryInitPromise) { await this._discoveryInitPromise; } const initialDiscoveryData = await this._discovery.initialData(); tokenEndpoint = initialDiscoveryData.authApi.defaultTokenUri; } } const res = await this._tokenRequest(tokenEndpoint, body); 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(); if (this._discovery) { this._discoveryInitPromise = this.initDiscovery(); // request new init data after refresh error and cache cleaned } } 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; const revokeEndpoint = await this._getRevokeEndpoint(); if (revokeEndpoint) { const authData = await this._auth.data(); const body: RevokeTokenBody = { token: authData.access_token, }; // Support to revoke token without client secret with client id in body res = await this._tokenRequest(revokeEndpoint, body); } await this._cache.clean(); if (this._discovery) { this._discoveryInitPromise = this.initDiscovery(); // request new init data after logout } this.emit(this.events.logoutSuccess, res); return res; } catch (e) { if (this._discovery) { this._discoveryInitPromise = this.initDiscovery(); // request new init data after logout error } this.emit(this.events.logoutError, e); throw e; } } private async _getRevokeEndpoint() { let revokeEndpoint = this._revokeEndpoint; if (!this._discovery || checkPathHasHttp(revokeEndpoint)) { return revokeEndpoint; } const discoveryData = await this._discovery.externalData(); const baseUri = discoveryData.authApi.baseUri; revokeEndpoint = `${baseUri}${revokeEndpoint}`; return revokeEndpoint; } public async inflateRequest(request: Request, options: SendOptions = {}): Promise<Request> { options = options || {}; let userAgent = this._userAgent; if (options.userAgent) { userAgent = `${options.userAgent} ${userAgent}`; } request.headers.set('X-User-Agent', 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) { let {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) { handleRateLimit = handleRateLimit || this._handleRateLimit; 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 (!handleRateLimit) {throw e;} } await delay(retryAfter); return this.sendRequest(this._client.createRequest(options), options); } } public async send(options: SendOptions = {}) { if (!options.skipAuthCheck && !options.skipDiscoveryCheck && this._discovery) { if (this._discoveryInitPromise) { await this._discoveryInitPromise; } const discoveryExpired = await this._discovery.externalDataExpired(); if (discoveryExpired) { await this._discovery.refreshExternalData(); } const discoveryData = await this._discovery.externalData(); if (!discoveryData) { throw new Error('Discovery data is missing'); } this._server = discoveryData.coreApi.baseUri; this._rcvServer = discoveryData.rcv.baseApiUri; if (discoveryData.tag) { options.headers = options.headers || {}; options.headers['Discovery-Tag'] = discoveryData.tag; } } //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); } /** * These methods refresh access token automatically if it's expired. * If you want to handle it yourself and disable auto refresh, place below code snippet in your code. * platform.ensureLoggedIn = async () => null; * For more details, please refer to https://medium.com/ringcentral-developers/how-to-disable-auto-token-refreshment-for-ringcentral-javascript-sdk-461d7982ed35 * You can also follow demo https://github.com/tylerlong/rc-js-sdk-no-auto-refresh-token-demo */ 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 patch(url, body?, query?, options?: SendOptions): Promise<Response> { return this.send({method: 'PATCH', 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> { const headers: TokenRequestHeaders = { 'Content-Type': Client._urlencodedContentType, }; if (this._clientSecret && this._clientSecret.length > 0) { headers.Authorization = this.basicAuthHeader(); } else { // Put client_id in body when no app secret body.client_id = this._clientId; } return this.send({ url, body, skipAuthCheck: true, method: 'POST', headers, }); } 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}` : ''); } public get discoveryInitPromise() { return this._discoveryInitPromise; } public get codeVerifier() { return this._codeVerifier; } public get userAgent() { return this._userAgent; } } export interface PlatformOptions extends AuthOptions { server?: string; clientId?: string; clientSecret?: string; redirectUri?: string; refreshDelayMs?: number; refreshHandicapMs?: number; clearCacheOnRefreshError?: boolean; appName?: string; appVersion?: string; additionalUserAgent?: string; tokenEndpoint?: string; revokeEndpoint?: string; authorizeEndpoint?: string; authProxy?: boolean; urlPrefix?: string; handleRateLimit?: boolean | number; enableDiscovery?: boolean; discoveryServer?: string; discoveryInitialEndpoint?: string; discoveryAuthorizedEndpoint?: string; discoveryAutoInit?: boolean; brandId?: 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; userAgent?: string; skipAuthCheck?: boolean; skipDiscoveryCheck?: 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; jwt?: string; access_token?: string; access_token_ttl?: number; refresh_token_ttl?: number; endpoint_id?: string; token_uri?: string; discovery_uri?: string; code_verifier?: string; redirect_uri?: string; } export interface LoginUrlOptions { state?: string; brandId?: string; display?: LoginUrlDisplay | string; prompt?: LoginUrlPrompt | string; implicit?: boolean; uiOptions?: string | string[]; uiLocales?: string; localeId?: string; usePKCE?: boolean; responseHint?: string | string[]; redirectUri?: string; loginHint?: string | 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; } export interface AuthorizationQuery { response_type: 'token' | 'code'; response_hint?: string | string[]; redirect_uri: string; client_id: string; state?: string; brand_id?: string; display?: LoginUrlDisplay | string; prompt?: LoginUrlPrompt | string; ui_options?: string | string[]; ui_locales?: string; localeId?: string; code_challenge?: string; code_challenge_method?: string; discovery?: boolean; login_hint?: string | string[]; } export interface TokenRequestHeaders { 'Content-Type': string; Authorization?: string; } export interface RefreshTokenBody { grant_type: 'refresh_token'; refresh_token: string; access_token_ttl: number; refresh_token_ttl: number; client_id?: string; } export interface RevokeTokenBody { token: string; client_id?: string; }