src/modules/identity-server.js
import uuidv4 from '../helpers/uuid';
import {checkIsLocal} from '../helpers/local';
/**
* Identity server
*
* @property {string} env - Environment to use, e.g: dev/stage/..
* @property {string} idmVersion - Identity server API version
* @property {string} sdkVersion - Identity SDK version
* @property {string} timeout - Default request timeout in seconds (0 to disable)
* @property {string} url - Base URL
*/
class IdentityServer {
/**
* IdentityServer constructor
*
* @param {Object} [options={}] - Options
* @param {string} [options.env='production'] - Environment to use, e.g: dev/stage/..
* @param {string} [options.idmVersion='v2'] - Identity server API version
* @param {string} [options.sdkVersion='test'] - Identity server API version
* @param {number} [options.timeout=20] - Default request timeout in seconds (0 to disable)
*/
constructor({
env = 'production',
idmVersion = 'v2',
useLocalConfig = false,
sdkVersion = 'test',
timeout = 20,
} = {}) {
Object.assign(this, {
config: {},
env,
idmVersion,
useLocalConfig,
sdkVersion,
timeout,
url: (useLocalConfig ? 'http://localhost:3000'
: env === 'production' ? 'https://id.nbc.com' : `https://${env}-id.nbc.com`),
});
}
/**
* Requests specified path (or URL) and returns a Promise of JSON parsed response body
*
* If response is not OK the returned Promise is rejected.
*
* If the returned Promise is rejected, the associated Error instance is decorated with status (number),
* statusText (string) and retry (function) properties.
* The retry function can be called to retry the request.
*
* If a body is specified it will be JSON stringified and a Content-Type: application/json header will be added.
*
* If a timeout is specified that will be used, otherwise the default IdentityServer timeout will be used.
*
* Besides these options, standard fetch init properties (e.g: method) are supported as options as well.
* @see https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
*
* @param {string} pathOrUrl - Request path or URL
* @param {Object} [options={}] - Options
* @param {Object} [options.headers={}] - Request headers
* @param {Object} [options.body] - Request body
* @param {number} [options.timeout] - Request timeout in seconds (0 to disable)
* @return {Promise} - Promise of JSON parsed response body
*/
request(pathOrUrl, options = {}) {
const {headers = {}, body, timeout = this.timeout, ...init} = options;
const retry = () => this.request(pathOrUrl, options);
// Convert headers
init.headers = new Headers(headers);
// Add JSON body and header if necessary
if (body) {
init.headers.set('Content-Type', 'application/json');
init.body = JSON.stringify(body);
}
// Add base server url if necessary
const url = new URL(pathOrUrl, this.url);
const promise = fetch(url.toString(), init).then((response) => {
const {ok, status, statusText} = response;
// Check headers to make sure response is JSON format
const contentType = response.headers.get('content-type');
if (!contentType || contentType.indexOf('application/json') === -1) {
const error = new Error('Response must be JSON');
throw Object.assign(error, {code: 422});
}
return response.json().then((responseBody) => {
if (ok) {
return responseBody;
} else {
// Reject if response status not OK and decorate error with any responseBody properties
const error = new Error(`Response not OK (${status} - ${statusText})`);
throw Object.assign(error, responseBody);
}
}).catch((error) => {
// And always decorate error with status properties and retry function
throw Object.assign(error, {status, statusText, retry});
});
});
if (!timeout) {
return promise;
}
return new Promise((resolve, reject) => {
setTimeout(() => {
// Like above, decorate error with status properties and retry function
const error = new Error(`Timeout after ${timeout} seconds`);
Object.assign(error, {
status: 0,
statusText: 'Timeout',
retry,
});
reject(error);
}, timeout * 1000);
promise.then(resolve, reject);
});
}
/**
* Returns an Identity Call URL with specified path
*
* This defaults to using the server url as base, but uses a special proxy instead when we are cross origin.
* Note: This proxy currently only supports envs: dev and stage
* TODO: Get rid of this workaround once Akamai supports adding /index.html and we can use *-id.nbc.com/sdk/
*
*
* @param {string} [path=''] - Identity Call path
* @return {string} - Identity Call URL with specified path
*/
getCallUrl(path = '') {
let base = this.url;
const local = checkIsLocal();
if (local||location.origin.endsWith('id.nbc.com-sdk.s3-website-us-east-1.amazonaws.com')) {
base = `https://us-central1-nbc-id-sdk.cloudfunctions.net/proxy/${this.env}/`;
}
return new URL(path, base).toString();
}
/**
* Makes an Identity server API call
*
* This adds some standard headers and then requests specified path (e.g: endpoint).
*
* See {@link IdentityServer#getSdkUrl} for details on the URL that is used.
* See {@link IdentityServer#request} for further supported options etc.
*
* @param {string} path - Request path
* @param {Object} [options={}] - Request options
* @param {Object} [options.headers={}] - Request headers
* @return {Promise} - Promise of response body
*/
call(path, {
headers = {},
...options
} = {}) {
return this.request(this.getCallUrl(path), {
headers: {
'Cache-Control': 'no-cache',
'idmVersion': this.idmVersion,
'idm_tx_ref': uuidv4(),
'X-IDM-Brand-Source': (this.config.showAttributes || {}).IDMBrandSource || '',
...headers,
},
...options,
});
}
/**
* Returns a SDK URL with specified path
*
* This defaults to using the server url + /sdk as base, but uses the s3-website instead when we are cross origin.
*
* This also adds the SDK version to the URL to serve as a cache buster.
*
* See {@link IdentityServer#request} for supported options etc.
*
* @param {String} [path=''] - SDK Request path
* @param {String} [customBase=null] - Custom SDK base URL
* @return {String} - SDK URL with specified path
*/
getSdkUrl(path = '', customBase = null) {
let base = customBase || (this.useLocalConfig ? `${this.url}/public/` : `${this.url}/sdk/`);
const url = new URL(path, base);
if (url.searchParams) {
url.searchParams.set('version', this.sdkVersion);
} else if (url.search) {
url.search.set('version', this.sdkVersion);
}
return url.toString();
}
/**
* Requests a response from an SDK path
*
* See {@link IdentityServer#getSdkUrl} for details on the URL that is used.
* See {@link IdentityServer#request} for supported options etc.
*
* @param {String} path - SDK Request path
* @param {String} [customBase=null] - Custom SDK base URL
* @param {Object} [options={}] - Request options
* @return {Promise} - Promise of JSON parsed response body
*/
sdk(path, customBase = null, options = {}) {
return this.request(this.getSdkUrl(path, customBase), options);
}
/**
* Set the config in the server scope
* @param {Object} config
*/
setConfig(config) {
this.config = config;
}
}
export default IdentityServer;