Source: box-node-sdk.ts

/**
 * @fileoverview Box SDK for Node.js
 */

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

import { EventEmitter } from 'events';
import * as qs from 'querystring';
import CCGAPISession = require('./sessions/ccg-session');
import APIRequestManager = require('./api-request-manager');
import BoxClient = require('./box-client');
import TokenManager = require('./token-manager');

const Config = require('./util/config'),
	BasicAPISession = require('./sessions/basic-session'),
	PersistentAPISession = require('./sessions/persistent-session'),
	AppAuthSession = require('./sessions/app-auth-session'),
	Webhooks = require('./managers/webhooks');

// ------------------------------------------------------------------------------
// Typedefs and Callbacks
// ------------------------------------------------------------------------------

/**
 * Object representing interface functions for PersistentClient to interact with the consumer app's central storage layer.
 * @typedef {Object} TokenStore
 * @property {ReadTokenInfoFromStore} read - read TokenInfo from app central store.
 * @property {WriteTokenInfoToStore} write - write TokenInfo to the app's central store.
 * @property {ClearTokenInfoFromStore} clear - delete TokenInfo from the app's central store.
 */

/**
 * Acquires TokenInfo from the consumer app's central store.
 * @typedef {Function} ReadTokenInfoFromStore
 * @param {Function} callback - err if store read issue occurred, otherwise propagates a TokenInfo object
 */

/**
 * Writes TokenInfo to the consumer app's central store
 * @typedef {Function} WriteTokenInfoToStore
 * @param {TokenInfo} tokenInfo - the token info to be written
 * @param {Function} callback - err if store write issue occurred, otherwise propagates null err
 *  and null result to indicate success
 */

/**
 * Clears TokenInfo from the consumer app's central store
 * @typedef {Function} ClearTokenInfoFromStore
 * @param {Function} callback - err if store delete issue occurred, otherwise propagates null err
 *  and null result to indicate success
 */

type TokenStore = object /* FIXME */;
type UserConfigurationOptions = object /* FIXME */;
type TokenRequestOptions = object /* FIXME */;
type CCGConfig = {
	boxSubjectType: "user" | "enterprise",
	boxSubjectId: string
}

// ------------------------------------------------------------------------------
// Private
// ------------------------------------------------------------------------------

// ------------------------------------------------------------------------------
// Public
// ------------------------------------------------------------------------------

/**
 * A backend NodeJS SDK to interact with the Box V2 API.
 * This is the single entry point for all SDK consumer interactions. This is the only file that a 3rd party app
 * should require. All other components are private and reached out to via this component.
 * 1. Provides getters to spawn client instances for users to interact with the Box API.
 * 2. Provides manual capability to acquire tokens via token grant endpoints.
 *    However, it is recommended to use clients to do this for you.
 * 3. Emits notification events about relevant request/response events. Useful for logging Box API interactions.
 *    Notification events: request retries, exceeding max retries, permanent failures.
 *
 * @param {UserConfigurationOptions} params User settings used to initialize and customize the SDK
 * @constructor
 */
class BoxSDKNode extends EventEmitter {
	accessLevels: any /* FIXME */;
	collaborationRoles: any /* FIXME */;
	CURRENT_USER_ID: any /* FIXME */;
	config: any /* FIXME */;
	_eventBus!: EventEmitter;

	requestManager!: APIRequestManager;
	tokenManager!: TokenManager;
	ccgSession!: CCGAPISession;

	/**
	 * Expose the BoxClient property enumerations to the SDK as a whole. This allows
	 * the consumer to access and use these values from anywhere in their application
	 * (like a helper) by requiring the SDK, instead of needing to pass the client.
	 */
	static accessLevels = BoxSDKNode.prototype.accessLevels;
	static collaborationRoles = BoxSDKNode.prototype.collaborationRoles;
	static CURRENT_USER_ID = BoxSDKNode.prototype.CURRENT_USER_ID;

	/**
	 * Expose Webhooks.validateMessage() to the SDK as a whole. This allows
	 * the consumer to call BoxSDK.validateWebhookMessage() by just requiring the SDK,
	 * instead of needing to create a client (which is not needed to validate messages).
	 */
	static validateWebhookMessage = Webhooks.validateMessage;

	constructor(params: UserConfigurationOptions) {
		super();

		const eventBus = new EventEmitter();

		const self = this;
		eventBus.on('response', function () {
			const args: any /* FIXME */ = [].slice.call(arguments);
			args.unshift('response');
			self.emit.apply(self, args);
		});

		// Setup the configuration with the given params
		this.config = new Config(params);
		this._eventBus = eventBus;
		this._setup();
	}

	/**
	 * Setup the SDK instance by instantiating necessary objects with current
	 * configuration values.
	 *
	 * @returns {void}
	 * @private
	 */
	_setup() {
		// Instantiate the request manager
		this.requestManager = new APIRequestManager(this.config, this._eventBus);

		// Initialize the rest of the SDK with the given configuration
		this.tokenManager = new TokenManager(this.config, this.requestManager);
		this.ccgSession = new CCGAPISession(
			this.config,
			this.tokenManager
		);
	}

	/**
	 * Gets the BoxSDKNode instance by passing boxAppSettings json downloaded from the developer console.
	 *
	 * @param {Object} appConfig boxAppSettings object retrieved from Dev Console.
	 * @returns {BoxSDKNode} an instance that has been preconfigured with the values from the Dev Console
	 */
	static getPreconfiguredInstance(appConfig: any /* FIXME */) {
		if (typeof appConfig.boxAppSettings !== 'object') {
			throw new TypeError(
				'Configuration does not include boxAppSettings object.'
			);
		}

		const boxAppSettings = appConfig.boxAppSettings;
		const webhooks = appConfig.webhooks;
		if (typeof webhooks === 'object') {
			Webhooks.setSignatureKeys(webhooks.primaryKey, webhooks.secondaryKey);
		}

		const params: {
			clientID?: string;
			clientSecret?: string;
			appAuth?: {
				keyID?: string;
				privateKey?: string;
				passphrase?: string;
			};
			enterpriseID?: string;
		} = {};

		if (typeof boxAppSettings.clientID === 'string') {
			params.clientID = boxAppSettings.clientID;
		}

		if (typeof boxAppSettings.clientSecret === 'string') {
			params.clientSecret = boxAppSettings.clientSecret;
		}

		// Only try to assign app auth settings if they are present
		// Some configurations do not include them (but might include other info, e.g. webhooks)
		if (
			typeof boxAppSettings.appAuth === 'object' &&
			boxAppSettings.appAuth.publicKeyID
		) {
			params.appAuth = {
				keyID: boxAppSettings.appAuth.publicKeyID, // Assign publicKeyID to keyID
				privateKey: boxAppSettings.appAuth.privateKey,
			};

			const passphrase = boxAppSettings.appAuth.passphrase;
			if (typeof passphrase === 'string') {
				params.appAuth.passphrase = passphrase;
			}
		}

		if (typeof appConfig.enterpriseID === 'string') {
			params.enterpriseID = appConfig.enterpriseID;
		}

		return new BoxSDKNode(params);
	}

	/**
	 * Updates the SDK configuration with new parameters.
	 *
	 * @param {UserConfigurationOptions} params User settings
	 * @returns {void}
	 */
	configure(params: UserConfigurationOptions) {
		this.config = this.config.extend(params);
		this._setup();
	}

	/**
	 * Returns a Box Client with a Basic API Session. The client is able to make requests on behalf of a user.
	 * A basic session has no access to a user's refresh token. Because of this, once the session's tokens
	 * expire the client cannot recover and a new session will need to be generated.
	 *
	 * @param {string} accessToken A user's Box API access token
	 * @returns {BoxClient} Returns a new Box Client paired to a new BasicAPISession
	 */
	getBasicClient(accessToken: string) {
		const apiSession = new BasicAPISession(accessToken, this.tokenManager);
		return new BoxClient(apiSession, this.config, this.requestManager);
	}

	/**
	 * Returns a Box Client with a Basic API Session. The client is able to make requests on behalf of a user.
	 * A basic session has no access to a user's refresh token. Because of this, once the session's tokens
	 * expire the client cannot recover and a new session will need to be generated.
	 *
	 * @param {string} accessToken A user's Box API access token
	 * @returns {BoxClient} Returns a new Box Client paired to a new BasicAPISession
	 */
	static getBasicClient(accessToken: string) {
		return new BoxSDKNode({
			clientID: '',
			clientSecret: '',
		}).getBasicClient(accessToken);
	}

	/**
	 * Returns a Box Client with a persistent API session. A persistent API session helps manage the user's tokens,
	 * and can refresh them automatically if the access token expires. If a central data-store is given, the session
	 * can read & write tokens to it.
	 *
	 * NOTE: If tokenInfo or tokenStore are formatted incorrectly, this method will throw an error. If you
	 * haven't explicitly created either of these objects or are otherwise not completly confident in their validity,
	 * you should wrap your call to getPersistentClient in a try-catch to handle any potential errors.
	 *
	 * @param {TokenInfo} tokenInfo A tokenInfo object to use for authentication
	 * @param {TokenStore} [tokenStore] An optional token store for reading/writing tokens to session
	 * @returns {BoxClient} Returns a new Box Client paired to a new PersistentAPISession
	 */
	getPersistentClient(tokenInfo: any /* FIXME */, tokenStore?: TokenStore) {
		const apiSession = new PersistentAPISession(
			tokenInfo,
			tokenStore,
			this.config,
			this.tokenManager
		);
		return new BoxClient(apiSession, this.config, this.requestManager);
	}

	/**
	 * Returns a Box Client configured to use Client Credentials Grant for a service account. Requires enterprise ID
	 * to be set when configuring SDK instance.
	 *
	 * @returns {BoxClient} Returns a new Box Client paired to a AnonymousAPISession. All Anonymous API Sessions share the
	 * same tokens, which allows them to refresh them efficiently and reduce load on both the application and
	 * the API.
	 */
	getAnonymousClient() {
		if (!this.config.enterpriseID) {
			throw new Error('Enterprise ID must be passed');
		}
		return this._getCCGClient({boxSubjectType: "enterprise", boxSubjectId: this.config.enterpriseID});
	}

	/**
	 * Returns a Box Client configured to use Client Credentials Grant for a specified user.
	 *
	 * @param userId the user ID to use when getting the access token
	 * @returns {BoxClient} Returns a new Box Client paired to a AnonymousAPISession. All Anonymous API Sessions share the
	 * same tokens, which allows them to refresh them efficiently and reduce load on both the application and
	 * the API.
	 */
	getCCGClientForUser(userId: string) {
		return this._getCCGClient({boxSubjectType: "user", boxSubjectId: userId})
	}

	_getCCGClient(config: CCGConfig) {
		const anonymousTokenManager = new TokenManager(
			{
				...this.config,
				...config
			},
			this.requestManager
		);
		const newAnonymousSession = new CCGAPISession(
			this.config,
			anonymousTokenManager
		);
		return new BoxClient(
			newAnonymousSession,
			this.config,
			this.requestManager
		);
	}

	/**
	 * Create a new client using App Auth for the given entity. This allows either
	 * managing App Users (as the enterprise) or performing operations as the App
	 * Users or Managed Users themselves (as a user).
	 *
	 * @param {string} type The type of entity to operate as, "enterprise" or "user"
	 * @param {string} [id] (Optional) The Box ID of the entity to operate as
	 * @param {TokenStore} [tokenStore] (Optional) the token store to use for caching tokens
	 * @returns {BoxClient} A new client authorized as the app user or enterprise
	 */
	getAppAuthClient(type: string, id?: string, tokenStore?: TokenStore) {
		if (type === 'enterprise' && !id) {
			if (this.config.enterpriseID) {
				id = this.config.enterpriseID;
			} else {
				throw new Error('Enterprise ID must be passed');
			}
		}

		const appAuthSession = new AppAuthSession(
			type,
			id,
			this.config,
			this.tokenManager,
			tokenStore
		);
		return new BoxClient(appAuthSession, this.config, this.requestManager);
	}

	/**
	 * Generate the URL for the authorize page to send users to for the first leg of
	 * the OAuth2 flow.
	 *
	 * @param {Object} params The OAuth2 parameters
	 * @returns {string} The authorize page URL
	 */
	getAuthorizeURL(params: { client_id?: string }) {
		params.client_id = this.config.clientID;

		return `${this.config.authorizeRootURL}/oauth2/authorize?${qs.stringify(
			params
		)}`;
	}

	/**
	 * Acquires token info using an authorization code
	 *
	 * @param {string} authorizationCode - authorization code issued by Box
	 * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant, null for default behavior
	 * @param {Function} [callback] - passed a TokenInfo object if tokens were granted successfully
	 * @returns {Promise<TokenInfo>} Promise resolving to the token info
	 */
	getTokensAuthorizationCodeGrant(
		authorizationCode: string,
		options?: TokenRequestOptions | null,
		callback?: Function
	) {
		return this.tokenManager
			.getTokensAuthorizationCodeGrant(
				authorizationCode,
				options as any /* FIXME */
			)
			.asCallback(callback);
	}

	/**
	 * Refreshes the access and refresh tokens for a given refresh token.
	 *
	 * @param {string} refreshToken - A valid OAuth refresh token
	 * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant, null for default behavior
	 * @param {Function} [callback] - passed a TokenInfo object if tokens were granted successfully
	 * @returns {Promise<TokenInfo>} Promise resolving to the token info
	 */
	getTokensRefreshGrant(
		refreshToken: string,
		options?: TokenRequestOptions | Function | null,
		callback?: Function
	) {
		if (typeof options === 'function') {
			callback = options;
			options = null;
		}

		return this.tokenManager
			.getTokensRefreshGrant(refreshToken, options as any /* FIXME */)
			.asCallback(callback);
	}

	/**
	 * Gets tokens for enterprise administration of app users
	 * @param {string} enterpriseID The ID of the enterprise to generate a token for
	 * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant, null for default behavior
	 * @param {Function} [callback] Passed the tokens if successful
	 * @returns {Promise<TokenInfo>} Promise resolving to the token info
	 */
	getEnterpriseAppAuthTokens(
		enterpriseID: string,
		options?: TokenRequestOptions | Function | null,
		callback?: Function
	) {
		if (typeof options === 'function') {
			callback = options;
			options = null;
		}

		if (!enterpriseID) {
			if (this.config.enterpriseID) {
				enterpriseID = this.config.enterpriseID;
			} else {
				throw new Error('Enterprise id must be passed');
			}
		}

		return this.tokenManager
			.getTokensJWTGrant('enterprise', enterpriseID, options as any /* FIXME */)
			.asCallback(callback);
	}

	/**
	 * Gets tokens for App Users via a JWT grant
	 * @param {string} userID The ID of the App User to generate a token for
	 * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant, null for default behavior
	 * @param {Function} [callback] Passed the tokens if successful
	 * @returns {Promise<TokentInfo>} Promise resolving to the token info
	 */
	getAppUserTokens(
		userID: string,
		options?: TokenRequestOptions | Function | null,
		callback?: Function
	) {
		if (typeof options === 'function') {
			callback = options;
			options = null;
		}

		return this.tokenManager
			.getTokensJWTGrant('user', userID, options as any /* FIXME */)
			.asCallback(callback);
	}

	/**
	 * Revokes a token pair associated with a given access or refresh token.
	 *
	 * @param {string} token - A valid access or refresh token to revoke
	 * @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant, null for default behavior
	 * @param {Function} [callback] - If err, revoke failed. Otherwise, revoke succeeded.
	 * @returns {Promise<TokenInfo>} Promise resolving to the token info
	 */
	revokeTokens(
		token: string,
		options?: TokenRequestOptions | Function | null,
		callback?: Function
	) {
		if (typeof options === 'function') {
			callback = options;
			options = null;
		}

		return this.tokenManager
			.revokeTokens(token, options as any /* FIXME */)
			.asCallback(callback);
	}
}

/**
 * Expose the BoxClient property enumerations to the SDK as a whole. This allows
 * the consumer to access and use these values from anywhere in their application
 * (like a helper) by requiring the SDK, instead of needing to pass the client.
 */
BoxSDKNode.prototype.accessLevels = BoxClient.prototype.accessLevels;
BoxSDKNode.prototype.collaborationRoles =
	BoxClient.prototype.collaborationRoles;
BoxSDKNode.prototype.CURRENT_USER_ID = BoxClient.prototype.CURRENT_USER_ID;

/** @module box-node-sdk/lib/box-node-sdk */
export = BoxSDKNode;