From 38439b9355c1e38e19d114bcbec26916923243a6 Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Thu, 27 Feb 2025 16:55:11 +0530 Subject: [PATCH 01/12] feat: added oauth support --- lib/contentstackClient.js | 35 +- lib/core/concurrency-queue.js | 55 ++- lib/core/contentstackHTTPClient.js | 22 ++ lib/core/oauthHandler.js | 322 ++++++++++++++++ package-lock.json | 574 +++++++++++++++++++++++++++-- package.json | 6 +- types/contentstackClient.d.ts | 3 + types/oauthHandler.d.ts | 67 ++++ webpack/webpack.nativescript.js | 6 +- webpack/webpack.node.js | 16 + webpack/webpack.react-native.js | 6 +- webpack/webpack.web.js | 12 +- 12 files changed, 1086 insertions(+), 38 deletions(-) create mode 100644 lib/core/oauthHandler.js create mode 100644 types/oauthHandler.d.ts diff --git a/lib/contentstackClient.js b/lib/contentstackClient.js index a83d0693..4ae95723 100644 --- a/lib/contentstackClient.js +++ b/lib/contentstackClient.js @@ -6,6 +6,7 @@ import { Organization } from './organization/index' import cloneDeep from 'lodash/cloneDeep' import { User } from './user/index' import error from './core/contentstackError' +import OAuthHandler from './core/oauthHandler' export default function contentstackClient ({ http }) { /** @@ -172,12 +173,44 @@ export default function contentstackClient ({ http }) { }, error) } + /** + * @description The oauth call is used to sign in to your Contentstack account and obtain the accesstoken. + * @memberof ContentstackClient + * @func oauth + * @param {Object} parameters - oauth parameters + * @prop {string} parameters.appId - appId of the application + * @prop {string} parameters.clientId - clientId of the application + * @prop {string} parameters.clientId - clientId of the application + * @prop {string} parameters.responseType - responseType + * @prop {string} parameters.scope - scope + * @prop {string} parameters.clientSecret - clientSecret of the application + * @returns {OAuthHandler} Instance of OAuthHandler + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client() + * + * client.oauth({ appId: , clientId: , redirectUri: , clientSecret: , responseType: , scope: }) + * .then(() => console.log('Logged in successfully')) + * + */ + function oauth(params = {}) { + http.defaults.versioningStrategy = "path"; + const appId = params.appId || '6400aa06db64de001a31c8a9'; + const clientId = params.clientId || 'Ie0FEfTzlfAHL4xM'; + const redirectUri = params.redirectUri || 'http://localhost:8184'; + const responseType = params.responseType || 'code'; + const scope = params.scope; + const clientSecret = params.clientSecret; + return new OAuthHandler(http, appId, clientId, redirectUri, clientSecret,responseType, scope); + } + return { login: login, logout: logout, getUser: getUser, stack: stack, organization: organization, - axiosInstance: http + axiosInstance: http, + oauth, } } diff --git a/lib/core/concurrency-queue.js b/lib/core/concurrency-queue.js index e27c5fd1..d977bb26 100644 --- a/lib/core/concurrency-queue.js +++ b/lib/core/concurrency-queue.js @@ -1,4 +1,5 @@ import Axios from 'axios' +import OAuthHandler from './oauthHandler' const defaultConfig = { maxRequests: 5, retryLimit: 5, @@ -75,17 +76,17 @@ export function ConcurrencyQueue ({ axios, config }) { request.formdata = request.data request.data = transformFormData(request) } - request.retryCount = request.retryCount || 0 - if (request.headers.authorization && request.headers.authorization !== undefined) { - if (this.config.authorization && this.config.authorization !== undefined) { - request.headers.authorization = this.config.authorization - request.authorization = this.config.authorization + if (axios?.oauth?.accessToken) { + const isTokenExpired = axios.oauth.tokenExpiryTime && Date.now() > axios.oauth.tokenExpiryTime; + if (isTokenExpired) { + return refreshAccessToken().catch((error) => { + throw new Error('Failed to refresh access token: ' + error.message); + }); } - delete request.headers.authtoken - } else if (request.headers.authtoken && request.headers.authtoken !== undefined && this.config.authtoken && this.config.authtoken !== undefined) { - request.headers.authtoken = this.config.authtoken - request.authtoken = this.config.authtoken - } + } + + request.retryCount = request?.retryCount || 0 + setAuthorizationHeaders(request); if (request.cancelToken === undefined) { const source = Axios.CancelToken.source() request.cancelToken = source.token @@ -108,6 +109,40 @@ export function ConcurrencyQueue ({ axios, config }) { }) } + const setAuthorizationHeaders = (request) => { + if (request.headers.authorization && request.headers.authorization !== undefined) { + if (this.config.authorization && this.config.authorization !== undefined) { + request.headers.authorization = this.config.authorization + request.authorization = this.config.authorization + } + delete request.headers.authtoken + } else if (request.headers.authtoken && request.headers.authtoken !== undefined && this.config.authtoken && this.config.authtoken !== undefined) { + request.headers.authtoken = this.config.authtoken + request.authtoken = this.config.authtoken + } else if (axios?.oauth?.accessToken) { + // If OAuth access token is available in axios instance + request.headers.authorization = `Bearer ${axios.oauth.accessToken}`; + request.authorization = `Bearer ${axios.oauth.accessToken}`; + delete request.headers.authtoken + } + } + + //Refresh Access Token + const refreshAccessToken = async () => { + try { + await new OAuthHandler(axios).refreshAccessToken(); + this.paused = false; // Resume the request queue once the token is refreshed + this.running.forEach(({ request, resolve, reject }) => { + resolve(request); // Retry the queued requests + }); + this.running = []; // Clear the running queue + } catch (error) { + this.paused = false; // Ensure we stop queueing requests on failure + this.running.forEach(({ reject }) => reject(error)); // Reject all queued requests + this.running = []; // Clear the running queue + } + }; + const delay = (time, isRefreshToken = false) => { if (!this.paused) { this.paused = true diff --git a/lib/core/contentstackHTTPClient.js b/lib/core/contentstackHTTPClient.js index 30f8dfad..f63f6e3c 100644 --- a/lib/core/contentstackHTTPClient.js +++ b/lib/core/contentstackHTTPClient.js @@ -65,9 +65,31 @@ export default function contentstackHttpClient (options) { config.basePath = `/${config.basePath.split('/').filter(Boolean).join('/')}` } const baseURL = config.endpoint || `${protocol}://${hostname}:${port}${config.basePath}/{api-version}` + let uiHostName = hostname; + let developerHubBaseUrl = hostname; + + if (hostname.endsWith('io')) { + uiHostName = hostname.replace('io', 'com'); + } + + if (hostname.startsWith('api')) { + uiHostName = uiHostName.replace('api', 'app'); + } + const uiBaseUrl = config.endpoint || `${protocol}://${uiHostName}`; + + developerHubBaseUrl = developerHubBaseUrl + ?.replace('api', 'developerhub-api') + .replace(/^dev\d+/, 'dev') // Replaces any 'dev1', 'dev2', etc. with 'dev' + .replace('io', 'com') + .replace(/^http/, '') // Removing `http` if already present + .replace(/^/, 'https://'); // Adds 'https://' at the start if not already there + + // set ui host name const axiosOptions = { // Axios baseURL, + uiBaseUrl, + developerHubBaseUrl, ...config, paramsSerializer: function (params) { var query = params.query diff --git a/lib/core/oauthHandler.js b/lib/core/oauthHandler.js new file mode 100644 index 00000000..cef3b400 --- /dev/null +++ b/lib/core/oauthHandler.js @@ -0,0 +1,322 @@ +/** + * @description OAuthHandler class to handle OAuth authorization and token management + * @class OAuthHandler + * @param {any} axiosInstance + * @param {any} appId + * @param {any} clientId + * @param {any} redirectUri + * @param {any} responseType='code' + * @param {any} clientSecret + * @param {any} scope=[] + * @returns {OAuthHandler} OAuthHandler instance + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + */ +export default class OAuthHandler { + constructor(axiosInstance, appId, clientId, redirectUri, clientSecret, responseType = 'code', scope = []) { + this.appId = appId; + this.clientId = clientId; + this.redirectUri = redirectUri; + this.responseType = responseType; + this.scope = scope.join(' '); + this.clientSecret = clientSecret; // Optional, if provided, PKCE will be skipped + this.OAuthBaseURL = axiosInstance.defaults.uiBaseUrl; + this.axiosInstance = axiosInstance; + this.axiosInstance.oauth = axiosInstance?.oauth || {}; + this.axiosInstance.oauth.redirectUri = redirectUri; + this.axiosInstance.oauth.clientId = clientId; + this.axiosInstance.oauth.appId = appId; + this.developerHubBaseUrl = axiosInstance.defaults.developerHubBaseUrl; + + // Only generate PKCE codeVerifier and codeChallenge if clientSecret is not provided + if (!this.clientSecret) { + this.codeVerifier = this.generateCodeVerifier(); + this.codeChallenge = null; + } + } + + // Helper function for setting common headers for API requests + _getHeaders() { + return { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + } + + // Generate a random string (code_verifier) + generateCodeVerifier(length = 128) { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + return Array.from({ length }, () => charset.charAt(Math.floor(Math.random() * charset.length))).join(''); + } + + async generateCodeChallenge(codeVerifier) { + // Check if in a browser environment or Node.js + if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) { + // Use the native Web Crypto API in the browser + const encoder = new TextEncoder(); + const data = encoder.encode(codeVerifier); + const hashBuffer = await window.crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const base64String = btoa(String.fromCharCode(...hashArray)); + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe Base64 + } else { + // Use crypto-browserify in Node.js environment + const crypto = require('crypto-browserify'); + const hash = crypto.createHash('sha256'); + hash.update(codeVerifier); + const hashBuffer = hash.digest(); + const base64String = hashBuffer.toString('base64'); + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe Base64 + } + } + + /** + * @description Authorize the user by redirecting to the OAuth provider's authorization page + * @memberof OAuthHandler + * @func authorize + * @returns {any} Authorization URL + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const authUrl = await oauthHandler.authorize(); + */ + async authorize() { + const baseUrl = `${this.OAuthBaseURL}/#!/apps/${this.appId}/authorize`; + const authUrl = new URL(baseUrl); + authUrl.searchParams.set('response_type', 'code'); // Using set() to avoid duplicate parameters + authUrl.searchParams.set('client_id', this.clientId); + if (this.clientSecret) { + return authUrl.toString(); + } else { + // PKCE flow: add code_challenge to the authorization URL + this.codeChallenge = await this.generateCodeChallenge(this.codeVerifier); + authUrl.searchParams.set('code_challenge', this.codeChallenge); + authUrl.searchParams.set('code_challenge_method', 'S256'); + return authUrl.toString(); + } + } + + /** + * @description Exchange the authorization code for an access token + * @memberof OAuthHandler + * @func exchangeCodeForToken + * @param {any} code - Authorization code received from the OAuth provider + * @returns {any} Token data + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const tokenData = await oauthHandler.exchangeCodeForToken('authorization_code'); + */ + async exchangeCodeForToken(code) { + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: this.redirectUri, + client_id: this.clientId, + ...(this.clientSecret ? { client_secret: this.clientSecret } : { code_verifier: this.codeVerifier }), // Choose between client_secret and code_verifier + }); + + this.axiosInstance.defaults.headers = this._getHeaders(); + try { + const response = await this.axiosInstance.post(`${this.OAuthBaseURL}/apps-api/apps/token`, body); + if (response.status !== 200) { + throw new Error('Failed to exchange authorization code for token'); + } + + this._saveTokens(response.data); + return response.data; + } catch (error) { + throw new Error('Failed to exchange authorization code for token:', error); + } + } + + // Save tokens and token expiry details + _saveTokens(data) { + this.axiosInstance.oauth.accessToken = data.access_token; + this.axiosInstance.oauth.refreshToken = data.refresh_token || this.axiosInstance.oauth.refreshToken; + this.axiosInstance.oauth.organizationUID = data.organization_uid; + this.axiosInstance.oauth.userUID = data.user_uid; + this.axiosInstance.oauth.tokenExpiryTime = Date.now() + (data.expires_in - 60) * 1000; // Store expiry time + } + + /** + * @description Refreshes the access token using the provided refresh token or the one stored in the axios instance. + * @memberof OAuthHandler + * @func refreshAccessToken + * @param {string|null} [providedRefreshToken=null] - The refresh token to use for refreshing the access token. If not provided, the stored refresh token will be used. + * @returns {Promise} - A promise that resolves to the response data containing the new access token, refresh token, and expiry time. + * @throws {Error} - Throws an error if no refresh token is available or if the token refresh request fails. + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const tokenData = await oauthHandler.refreshAccessToken(); + */ + async refreshAccessToken(providedRefreshToken = null) { + const refreshToken = providedRefreshToken || this.axiosInstance.oauth.refreshToken; + + if (!refreshToken) { + throw new Error('No refresh token available. Please authenticate first.'); + } + + const body = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + client_id: this.axiosInstance.oauth.clientId, + redirect_uri: this.axiosInstance.oauth.redirectUri, + }); + + this.axiosInstance.defaults.headers = this._getHeaders(); + try { + const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/apps/token`, body); + + if (response.status !== 200) { + throw new Error(`Failed to refresh access token. Status: ${response.status}`); + } + + const data = response.data; + this.axiosInstance.oauth.accessToken = data.access_token; + this.axiosInstance.oauth.refreshToken = data.refresh_token || this.axiosInstance.oauth.refreshToken; // Optionally update refresh token + this.axiosInstance.oauth.tokenExpiryTime = Date.now() + (data.expires_in - 60) * 1000; // Update expiry time + return data; + } catch (error) { + throw new Error('Failed to refresh access token:', error); + } + } + + /** + * @description Logs out the user by revoking the OAuth app authorization + * @memberof OAuthHandler + * @func logout + * @returns {Promise} - A promise that resolves to a success message if the logout was successful. + * @throws {Error} - Throws an error if the logout request fails. + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const resp = await oauthHandler.logout(); + */ + async logout() { + try { + const authorizationId = await this.getOauthAppAuthorization(); + await this.revokeOauthAppAuthorization(authorizationId); + this.axiosInstance.oauth.accessToken = null; + this.axiosInstance.oauth.refreshToken = null; + this.axiosInstance.oauth.tokenExpiryTime = null; + delete this.axiosInstance.oauth; + return 'Logged out successfully'; + } catch (error) { + throw new Error('Failed to log out:', error); + } + } + + /** + * @description Get the current access token + * @memberof OAuthHandler + * @func getAccessToken + * @returns {any} + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const accessToken = oauthHandler.getAccessToken(); + */ + getAccessToken() { + return this.axiosInstance.oauth.accessToken; + } + + /** + * @description Handles the redirect URL after OAuth authorization + * @memberof OAuthHandler + * @func handleRedirect + * @async + * @param {string} url - The URL to handle after the OAuth authorization + * @returns {Promise} - A promise that resolves if the redirect URL is successfully handled + * @throws {Error} - Throws an error if the authorization code is not found in the redirect URL + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * await oauthHandler.handleRedirect('http://localhost:8184?code=authorization_code'); + */ + async handleRedirect(url) { + const urlParams = new URLSearchParams(new URL(url).search); + const code = urlParams.get('code'); + + if (code) { + try { + await this.exchangeCodeForToken(code); + } catch (error) { + throw new Error('OAuth Authentication failed:', error); + } + } else { + throw new Error('Authorization code not found in redirect URL.'); + } + } + + /** + * @description Get the OAuth app authorization for the current user + * @memberof OAuthHandler + * @func getOauthAppAuthorization + * @returns {any} + */ + async getOauthAppAuthorization() { + const headers = { + authorization: `Bearer ${this.axiosInstance.oauth.accessToken}`, + organization_uid: this.axiosInstance.oauth.organizationUID, + 'Content-type': 'application/json', + }; + + this.axiosInstance.defaults.headers = headers; + try { + const res = await this.axiosInstance.get( + `${this.developerHubBaseUrl}/manifests/${this.axiosInstance.oauth.appId}/authorizations`, + ); + + const data = res.data; + if (data?.data?.length > 0) { + const userUid = this.axiosInstance.oauth.userUID; + const currentUserAuthorization = data?.data?.filter((element) => element.user.uid === userUid) || []; + if (currentUserAuthorization.length === 0) { + throw new Error('No authorizations found for current user!'); + } + return currentUserAuthorization[0].authorization_uid; // filter authorizations by current logged in user + } else { + throw new Error('No authorizations found for the app!'); + } + } catch (error) { + throw new Error('Failed to get authorizations:', error); + } + } + + /** + * @description Revoke the OAuth app authorization for the current user + * @memberof OAuthHandler + * @func revokeOauthAppAuthorization + * @param {any} authorizationId + * @returns {any} + */ + async revokeOauthAppAuthorization(authorizationId) { + if (authorizationId?.length > 1) { + const headers = { + authorization: `Bearer ${this.axiosInstance.oauth.accessToken}`, + organization_uid: this.axiosInstance.oauth.organizationUID, + 'Content-type': 'application/json', + }; + + this.axiosInstance.defaults.headers = headers; + try { + const res = await this.axiosInstance.delete( + `${this.developerHubBaseUrl}/manifests/${this.axiosInstance.oauth.appId}/authorizations/${authorizationId}`, + ); + + return res.data; + } catch (error) { + throw new Error('Failed to revoke authorization:', error); + } + } + } +} diff --git a/package-lock.json b/package-lock.json index a9e98546..1e6eb0a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,14 @@ "version": "1.19.2", "license": "MIT", "dependencies": { + "assert": "^2.1.0", "axios": "^1.7.9", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.1", "form-data": "^4.0.1", "lodash": "^4.17.21", - "qs": "^6.14.0" + "qs": "^6.14.0", + "stream-browserify": "^3.0.0" }, "devDependencies": { "@babel/cli": "^7.26.4", @@ -4050,6 +4054,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -4080,7 +4111,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -4630,6 +4660,25 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -4660,6 +4709,11 @@ "dev": true, "license": "MIT" }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, "node_modules/body-parser": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.2.tgz", @@ -4786,6 +4840,11 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -4793,6 +4852,73 @@ "dev": true, "license": "ISC" }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -4849,6 +4975,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -4863,6 +5012,11 @@ "dev": true, "license": "MIT" }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4922,7 +5076,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -5130,6 +5283,18 @@ "node": ">=8" } }, + "node_modules/cipher-base": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -5370,6 +5535,50 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5385,6 +5594,31 @@ "node": ">= 8" } }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5555,7 +5789,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -5573,7 +5806,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -5639,6 +5871,15 @@ "node": ">= 0.8" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5680,6 +5921,21 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, "node_modules/docdash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/docdash/-/docdash-1.2.0.tgz", @@ -5758,6 +6014,25 @@ "dev": true, "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, "node_modules/emittery": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", @@ -6561,6 +6836,15 @@ "node": ">=0.8.x" } }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6941,7 +7225,6 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -7404,7 +7687,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -7445,7 +7727,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7457,6 +7738,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -7506,6 +7808,16 @@ "he": "bin/he" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7553,6 +7865,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7705,7 +8036,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -7757,7 +8087,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -7889,7 +8218,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8006,7 +8334,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -8047,6 +8374,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -8154,7 +8496,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8250,7 +8591,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -10484,6 +10824,16 @@ "node": ">= 0.4" } }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -10545,6 +10895,23 @@ "node": ">=8.6" } }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, "node_modules/mime-db": { "version": "1.53.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", @@ -10578,6 +10945,16 @@ "node": ">=6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -11511,7 +11888,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -11528,7 +11904,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11538,7 +11913,6 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", @@ -11849,6 +12223,22 @@ "node": ">=6" } }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -11966,6 +12356,21 @@ "node": "*" } }, + "node_modules/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -12112,7 +12517,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12167,6 +12571,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/process-on-spawn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", @@ -12243,6 +12652,24 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12313,12 +12740,20 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -12365,6 +12800,30 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -12811,6 +13270,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, "node_modules/router": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", @@ -12878,7 +13346,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -12916,7 +13383,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -13060,7 +13526,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -13112,6 +13577,18 @@ "dev": true, "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -13406,6 +13883,41 @@ "node": ">= 0.8" } }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-browserify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -14401,6 +14913,23 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -14765,7 +15294,6 @@ "version": "1.1.18", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", diff --git a/package.json b/package.json index 00e84570..cd6bfa1f 100644 --- a/package.json +++ b/package.json @@ -52,10 +52,14 @@ "author": "Contentstack", "license": "MIT", "dependencies": { + "assert": "^2.1.0", "axios": "^1.7.9", + "buffer": "^6.0.3", + "crypto-browserify": "^3.12.1", "form-data": "^4.0.1", "lodash": "^4.17.21", - "qs": "^6.14.0" + "qs": "^6.14.0", + "stream-browserify": "^3.0.0" }, "keywords": [ "contentstack management api", diff --git a/types/contentstackClient.d.ts b/types/contentstackClient.d.ts index 5374df27..2689c0ed 100644 --- a/types/contentstackClient.d.ts +++ b/types/contentstackClient.d.ts @@ -6,6 +6,7 @@ import { Response } from './contentstackCollection' import { Stack, StackConfig, StackDetails } from './stack' import { Organization, Organizations } from './organization' import { Queryable } from './utility/operations' +import OAuthHandler from './oauthHandler' export interface ProxyConfig { host: string @@ -67,6 +68,8 @@ export interface ContentstackClient { organization(): Organizations organization(uid: string): Organization + + oauth(params?: any): OAuthHandler } export function client(config?: ContentstackConfig): ContentstackClient \ No newline at end of file diff --git a/types/oauthHandler.d.ts b/types/oauthHandler.d.ts new file mode 100644 index 00000000..7d319347 --- /dev/null +++ b/types/oauthHandler.d.ts @@ -0,0 +1,67 @@ +// Interface to define the structure of the OAuth response +interface OAuthResponse { + access_token: string; + refresh_token?: string; + expires_in: number; + organization_uid: string; + user_uid: string; + token_type: string, + location: string, + region: string, + authorization_type: string, + stack_api_key: string +} + +export default class OAuthHandler { + /** + * Generate the authorization URL for OAuth + * @returns A promise that resolves to the authorization URL + */ + authorize(): Promise; + + /** + * Exchange the authorization code for an access token + * @param code - The authorization code + * @returns A promise that resolves to the OAuth response + */ + exchangeCodeForToken(code: string): Promise; + + /** + * Refresh the access token using the refresh token + * @param providedRefreshToken - The refresh token to use (optional) + * @returns A promise that resolves to the OAuth response + */ + refreshAccessToken(providedRefreshToken?: string): Promise; + + /** + * Log the user out by revoking the OAuth app authorization + * @returns A promise that resolves to a success message + */ + logout(): Promise; + + /** + * Get the current access token + * @returns The access token + */ + getAccessToken(): string; + + /** + * Handle the OAuth redirect URL and exchange the authorization code for a token + * @param url - The redirect URL containing the authorization code + * @returns A promise that resolves when the OAuth code is exchanged for a token + */ + handleRedirect(url: string): Promise; + + /** + * Get the OAuth app authorization for the current user + * @returns A promise that resolves to the authorization UID + */ + getOauthAppAuthorization(): Promise; + + /** + * Revoke the OAuth app authorization + * @param authorizationId - The authorization ID to revoke + * @returns A promise that resolves to the response from the API + */ + revokeOauthAppAuthorization(authorizationId: string): Promise; +} diff --git a/webpack/webpack.nativescript.js b/webpack/webpack.nativescript.js index 7ab1290d..22657d2f 100644 --- a/webpack/webpack.nativescript.js +++ b/webpack/webpack.nativescript.js @@ -12,7 +12,11 @@ module.exports = function (options) { }, resolve: { fallback: { - os: false + os: false, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + assert: require.resolve('assert'), + buffer: require.resolve('buffer') } }, module: { diff --git a/webpack/webpack.node.js b/webpack/webpack.node.js index bb3f87df..9a7890ca 100644 --- a/webpack/webpack.node.js +++ b/webpack/webpack.node.js @@ -10,6 +10,22 @@ module.exports = function (options) { filename: 'contentstack-management.js' }, target: 'node', + resolve: { + fallback: { + os: require.resolve('os-browserify/browser'), + fs: false, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + assert: require.resolve('assert'), + buffer: require.resolve('buffer') + }, + alias:{ + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + assert: require.resolve('assert'), + buffer: require.resolve('buffer') + } + }, module: { rules: [{ test: /\.js?$/, diff --git a/webpack/webpack.react-native.js b/webpack/webpack.react-native.js index 727d8aa5..c66d20ed 100644 --- a/webpack/webpack.react-native.js +++ b/webpack/webpack.react-native.js @@ -12,7 +12,11 @@ module.exports = function (options) { }, resolve: { fallback: { - os: false + os: false, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + assert: require.resolve('assert'), + buffer: require.resolve('buffer') } }, module: { diff --git a/webpack/webpack.web.js b/webpack/webpack.web.js index 913ecbb7..8a3db962 100644 --- a/webpack/webpack.web.js +++ b/webpack/webpack.web.js @@ -16,7 +16,17 @@ module.exports = function (options) { resolve: { fallback: { os: require.resolve('os-browserify/browser'), - fs: false + fs: false, + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + assert: require.resolve('assert'), + buffer: require.resolve('buffer') + }, + alias:{ + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + assert: require.resolve('assert'), + buffer: require.resolve('buffer') } }, module: { From 13f2bef6442d4fc2c902077d3ab175e982840acd Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Fri, 28 Feb 2025 17:56:43 +0530 Subject: [PATCH 02/12] fix: snyk issue --- lib/core/oauthHandler.js | 13 +- package-lock.json | 439 +------------------------------- package.json | 1 - webpack/webpack.nativescript.js | 2 +- webpack/webpack.node.js | 8 +- webpack/webpack.react-native.js | 2 +- webpack/webpack.web.js | 8 +- 7 files changed, 26 insertions(+), 447 deletions(-) diff --git a/lib/core/oauthHandler.js b/lib/core/oauthHandler.js index cef3b400..6884ef83 100644 --- a/lib/core/oauthHandler.js +++ b/lib/core/oauthHandler.js @@ -61,15 +61,18 @@ export default class OAuthHandler { const base64String = btoa(String.fromCharCode(...hashArray)); return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe Base64 } else { - // Use crypto-browserify in Node.js environment - const crypto = require('crypto-browserify'); + // In Node.js: Use the native `crypto` module for hashing + const crypto = require('crypto'); + const hash = crypto.createHash('sha256'); hash.update(codeVerifier); const hashBuffer = hash.digest(); - const base64String = hashBuffer.toString('base64'); + + // Convert to Base64 and URL-safe Base64 + let base64String = hashBuffer.toString('base64'); return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe Base64 - } - } + } +} /** * @description Authorize the user by redirecting to the OAuth provider's authorization page diff --git a/package-lock.json b/package-lock.json index 1e6eb0a1..f4bc9dcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "assert": "^2.1.0", "axios": "^1.7.9", "buffer": "^6.0.3", - "crypto-browserify": "^3.12.1", "form-data": "^4.0.1", "lodash": "^4.17.21", "qs": "^6.14.0", @@ -4054,21 +4053,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" - }, "node_modules/assert": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", @@ -4709,11 +4693,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, "node_modules/body-parser": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.2.tgz", @@ -4840,11 +4819,6 @@ "node": ">=8" } }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" - }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -4852,73 +4826,6 @@ "dev": true, "license": "ISC" }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", - "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", - "dependencies": { - "bn.js": "^5.2.1", - "randombytes": "^2.1.0", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", - "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -5012,11 +4919,6 @@ "dev": true, "license": "MIT" }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5283,18 +5185,6 @@ "node": ">=8" } }, - "node_modules/cipher-base": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", - "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -5535,50 +5425,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5594,31 +5440,6 @@ "node": ">= 8" } }, - "node_modules/crypto-browserify": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", - "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", - "dependencies": { - "browserify-cipher": "^1.0.1", - "browserify-sign": "^4.2.3", - "create-ecdh": "^4.0.4", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "diffie-hellman": "^5.0.3", - "hash-base": "~3.0.4", - "inherits": "^2.0.4", - "pbkdf2": "^3.1.2", - "public-encrypt": "^4.0.3", - "randombytes": "^2.1.0", - "randomfill": "^1.0.4" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5871,15 +5692,6 @@ "node": ">= 0.8" } }, - "node_modules/des.js": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", - "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5921,21 +5733,6 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" - }, "node_modules/docdash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/docdash/-/docdash-1.2.0.tgz", @@ -6014,25 +5811,6 @@ "dev": true, "license": "ISC" }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" - }, "node_modules/emittery": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", @@ -6836,15 +6614,6 @@ "node": ">=0.8.x" } }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7738,27 +7507,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -7808,16 +7556,6 @@ "he": "bin/he" } }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -10824,16 +10562,6 @@ "node": ">= 0.4" } }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -10895,23 +10623,6 @@ "node": ">=8.6" } }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" - }, "node_modules/mime-db": { "version": "1.53.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", @@ -10945,16 +10656,6 @@ "node": ">=6" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -12223,22 +11924,6 @@ "node": ">=6" } }, - "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -12356,21 +12041,6 @@ "node": "*" } }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -12571,11 +12241,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "node_modules/process-on-spawn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", @@ -12652,24 +12317,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12740,20 +12387,12 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -12801,29 +12440,18 @@ "license": "MIT" }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -13270,15 +12898,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, "node_modules/router": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", @@ -13577,18 +13196,6 @@ "dev": true, "license": "ISC" }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -13892,32 +13499,14 @@ "readable-stream": "^3.5.0" } }, - "node_modules/stream-browserify/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", diff --git a/package.json b/package.json index cd6bfa1f..a5db1347 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "assert": "^2.1.0", "axios": "^1.7.9", "buffer": "^6.0.3", - "crypto-browserify": "^3.12.1", "form-data": "^4.0.1", "lodash": "^4.17.21", "qs": "^6.14.0", diff --git a/webpack/webpack.nativescript.js b/webpack/webpack.nativescript.js index 22657d2f..965876a5 100644 --- a/webpack/webpack.nativescript.js +++ b/webpack/webpack.nativescript.js @@ -13,7 +13,7 @@ module.exports = function (options) { resolve: { fallback: { os: false, - crypto: require.resolve('crypto-browserify'), + crypto: false, stream: require.resolve('stream-browserify'), assert: require.resolve('assert'), buffer: require.resolve('buffer') diff --git a/webpack/webpack.node.js b/webpack/webpack.node.js index 9a7890ca..aa0cbb11 100644 --- a/webpack/webpack.node.js +++ b/webpack/webpack.node.js @@ -14,17 +14,11 @@ module.exports = function (options) { fallback: { os: require.resolve('os-browserify/browser'), fs: false, - crypto: require.resolve('crypto-browserify'), + crypto: false, stream: require.resolve('stream-browserify'), assert: require.resolve('assert'), buffer: require.resolve('buffer') }, - alias:{ - crypto: require.resolve('crypto-browserify'), - stream: require.resolve('stream-browserify'), - assert: require.resolve('assert'), - buffer: require.resolve('buffer') - } }, module: { rules: [{ diff --git a/webpack/webpack.react-native.js b/webpack/webpack.react-native.js index c66d20ed..e2aaa4c3 100644 --- a/webpack/webpack.react-native.js +++ b/webpack/webpack.react-native.js @@ -13,7 +13,7 @@ module.exports = function (options) { resolve: { fallback: { os: false, - crypto: require.resolve('crypto-browserify'), + crypto: false, stream: require.resolve('stream-browserify'), assert: require.resolve('assert'), buffer: require.resolve('buffer') diff --git a/webpack/webpack.web.js b/webpack/webpack.web.js index 8a3db962..e20f7016 100644 --- a/webpack/webpack.web.js +++ b/webpack/webpack.web.js @@ -17,17 +17,11 @@ module.exports = function (options) { fallback: { os: require.resolve('os-browserify/browser'), fs: false, - crypto: require.resolve('crypto-browserify'), + crypto: false, stream: require.resolve('stream-browserify'), assert: require.resolve('assert'), buffer: require.resolve('buffer') }, - alias:{ - crypto: require.resolve('crypto-browserify'), - stream: require.resolve('stream-browserify'), - assert: require.resolve('assert'), - buffer: require.resolve('buffer') - } }, module: { rules: [{ From aa892bad54c07c5ffbe68ad207d5100f62e4dc98 Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Sun, 2 Mar 2025 00:07:38 +0530 Subject: [PATCH 03/12] feat: added getter & setter methods for access token, refresh --- lib/core/oauthHandler.js | 212 +++++++++++++++++++++++++++++++++------ 1 file changed, 179 insertions(+), 33 deletions(-) diff --git a/lib/core/oauthHandler.js b/lib/core/oauthHandler.js index 6884ef83..699abdc8 100644 --- a/lib/core/oauthHandler.js +++ b/lib/core/oauthHandler.js @@ -1,3 +1,5 @@ +import errorFormatter from './contentstackError'; + /** * @description OAuthHandler class to handle OAuth authorization and token management * @class OAuthHandler @@ -61,8 +63,8 @@ export default class OAuthHandler { const base64String = btoa(String.fromCharCode(...hashArray)); return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe Base64 } else { - // In Node.js: Use the native `crypto` module for hashing - const crypto = require('crypto'); + // In Node.js: Use the native `crypto` module for hashing + const crypto = require('crypto'); const hash = crypto.createHash('sha256'); hash.update(codeVerifier); @@ -71,8 +73,8 @@ export default class OAuthHandler { // Convert to Base64 and URL-safe Base64 let base64String = hashBuffer.toString('base64'); return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe Base64 - } -} + } + } /** * @description Authorize the user by redirecting to the OAuth provider's authorization page @@ -86,18 +88,22 @@ export default class OAuthHandler { * const authUrl = await oauthHandler.authorize(); */ async authorize() { - const baseUrl = `${this.OAuthBaseURL}/#!/apps/${this.appId}/authorize`; - const authUrl = new URL(baseUrl); - authUrl.searchParams.set('response_type', 'code'); // Using set() to avoid duplicate parameters - authUrl.searchParams.set('client_id', this.clientId); - if (this.clientSecret) { - return authUrl.toString(); - } else { - // PKCE flow: add code_challenge to the authorization URL - this.codeChallenge = await this.generateCodeChallenge(this.codeVerifier); - authUrl.searchParams.set('code_challenge', this.codeChallenge); - authUrl.searchParams.set('code_challenge_method', 'S256'); - return authUrl.toString(); + try { + const baseUrl = `${this.OAuthBaseURL}/#!/apps/${this.appId}/authorize`; + const authUrl = new URL(baseUrl); + authUrl.searchParams.set('response_type', 'code'); // Using set() to avoid duplicate parameters + authUrl.searchParams.set('client_id', this.clientId); + if (this.clientSecret) { + return authUrl.toString(); + } else { + // PKCE flow: add code_challenge to the authorization URL + this.codeChallenge = await this.generateCodeChallenge(this.codeVerifier); + authUrl.searchParams.set('code_challenge', this.codeChallenge); + authUrl.searchParams.set('code_challenge_method', 'S256'); + return authUrl.toString(); + } + } catch (error) { + errorFormatter(error); } } @@ -125,14 +131,11 @@ export default class OAuthHandler { this.axiosInstance.defaults.headers = this._getHeaders(); try { const response = await this.axiosInstance.post(`${this.OAuthBaseURL}/apps-api/apps/token`, body); - if (response.status !== 200) { - throw new Error('Failed to exchange authorization code for token'); - } this._saveTokens(response.data); return response.data; } catch (error) { - throw new Error('Failed to exchange authorization code for token:', error); + errorFormatter(error); } } @@ -176,17 +179,13 @@ export default class OAuthHandler { try { const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/apps/token`, body); - if (response.status !== 200) { - throw new Error(`Failed to refresh access token. Status: ${response.status}`); - } - const data = response.data; this.axiosInstance.oauth.accessToken = data.access_token; this.axiosInstance.oauth.refreshToken = data.refresh_token || this.axiosInstance.oauth.refreshToken; // Optionally update refresh token this.axiosInstance.oauth.tokenExpiryTime = Date.now() + (data.expires_in - 60) * 1000; // Update expiry time return data; } catch (error) { - throw new Error('Failed to refresh access token:', error); + errorFormatter(error); } } @@ -206,13 +205,10 @@ export default class OAuthHandler { try { const authorizationId = await this.getOauthAppAuthorization(); await this.revokeOauthAppAuthorization(authorizationId); - this.axiosInstance.oauth.accessToken = null; - this.axiosInstance.oauth.refreshToken = null; - this.axiosInstance.oauth.tokenExpiryTime = null; - delete this.axiosInstance.oauth; + this.axiosInstance.oauth = {}; // Clear OAuth data return 'Logged out successfully'; } catch (error) { - throw new Error('Failed to log out:', error); + errorFormatter(error); } } @@ -231,6 +227,156 @@ export default class OAuthHandler { return this.axiosInstance.oauth.accessToken; } + /** + * @description Get the refresh token from the axios instance + * @memberof OAuthHandler + * @func getRefreshToken + * @returns {string|null} + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const refreshToken = oauthHandler.getRefreshToken(); + */ + getRefreshToken() { + return this.axiosInstance.oauth.refreshToken || null; + } + + /** + * @description Get the organization UID from the axios instance + * @memberof OAuthHandler + * @func getOrganizationUID + * @returns {string|null} + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const orgUID = oauthHandler.getOrganizationUID(); + */ + getOrganizationUID() { + return this.axiosInstance.oauth.organizationUID || null; + } + + /** + * @description Get the user UID from the axios instance + * @memberof OAuthHandler + * @func getUserUID + * @returns {string|null} + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const userId = oauthHandler.getUserUID(); + */ + getUserUID() { + return this.axiosInstance.oauth.userUID || null; + } + + /** + * @description Get the token expiry time from the axios instance + * @memberof OAuthHandler + * @func getTokenExpiryTime + * @returns {number|null} + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * const expiryTime = oauthHandler.getTokenExpiryTime(); + */ + getTokenExpiryTime() { + return this.axiosInstance.oauth.tokenExpiryTime || null; + } + + /** + * @description Set the access token in the axios instance + * @memberof OAuthHandler + * @func setAccessToken + * @param {*} token + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * oauthHandler.setAccessToken('accessToken'); + */ + setAccessToken(token) { + if (!token) { + throw new Error('Access token is required'); + } + this.axiosInstance.oauth.accessToken = token; + } + + /** + * @description Set the refresh token in the axios instance + * @memberof OAuthHandler + * @func setRefreshToken + * @param {*} token + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * oauthHandler.setRefreshToken('refreshToken'); + */ + setRefreshToken(token) { + if (!token) { + throw new Error('Refresh token is required'); + } + this.axiosInstance.oauth.refreshToken = token; + } + + /** + * @description Set the organization UID in the axios instance + * @memberof OAuthHandler + * @func setOrganizationUID + * @param {*} organizationUID + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * oauthHandler.setOrganizationUID('organizationUID'); + */ + setOrganizationUID(organizationUID) { + if (!organizationUID) { + throw new Error('Organization UID is required'); + } + this.axiosInstance.oauth.organizationUID = organizationUID; + } + + /** + * @description Set the user UID in the axios instance + * @memberof OAuthHandler + * @func setUserUID + * @param {*} userUID + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * oauthHandler.setUserUID('userID'); + */ + setUserUID(userUID) { + if (!userUID) { + throw new Error('User UID is required'); + } + this.axiosInstance.oauth.userUID = userUID; + } + + /** + * @description Set the token expiry time in the axios instance + * @memberof OAuthHandler + * @func setTokenExpiryTime + * @param {*} expiryTime + * @example + * import * as contentstack from '@contentstack/management' + * const client = contentstack.client(); + * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); + * oauthHandler.setTokenExpiryTime('expiryTime'); // date format + */ + setTokenExpiryTime(expiryTime) { + if (!expiryTime) { + throw new Error('Token expiry time is required'); + } + this.axiosInstance.oauth.tokenExpiryTime = expiryTime; + } + /** * @description Handles the redirect URL after OAuth authorization * @memberof OAuthHandler @@ -253,7 +399,7 @@ export default class OAuthHandler { try { await this.exchangeCodeForToken(code); } catch (error) { - throw new Error('OAuth Authentication failed:', error); + errorFormatter(error); } } else { throw new Error('Authorization code not found in redirect URL.'); @@ -291,7 +437,7 @@ export default class OAuthHandler { throw new Error('No authorizations found for the app!'); } } catch (error) { - throw new Error('Failed to get authorizations:', error); + errorFormatter(error); } } @@ -318,7 +464,7 @@ export default class OAuthHandler { return res.data; } catch (error) { - throw new Error('Failed to revoke authorization:', error); + errorFormatter(error); } } } From 7efbab245d341a00687bf1bda1a821ed0df1276a Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Fri, 7 Mar 2025 16:56:15 +0530 Subject: [PATCH 04/12] feat: retry the requests that were pending due to token expiration --- lib/core/concurrency-queue.js | 13 ++++--- lib/core/oauthHandler.js | 3 ++ types/oauthHandler.d.ts | 64 ++++++++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/lib/core/concurrency-queue.js b/lib/core/concurrency-queue.js index d977bb26..e3286913 100644 --- a/lib/core/concurrency-queue.js +++ b/lib/core/concurrency-queue.js @@ -130,18 +130,23 @@ export function ConcurrencyQueue ({ axios, config }) { //Refresh Access Token const refreshAccessToken = async () => { try { + // Try to refresh the token await new OAuthHandler(axios).refreshAccessToken(); this.paused = false; // Resume the request queue once the token is refreshed + + // Retry the requests that were pending due to token expiration this.running.forEach(({ request, resolve, reject }) => { - resolve(request); // Retry the queued requests + // Retry the request + axios(request).then(resolve).catch(reject); }); - this.running = []; // Clear the running queue + this.running = []; // Clear the running queue after retrying requests } catch (error) { - this.paused = false; // Ensure we stop queueing requests on failure + this.paused = false; // stop queueing requests on failure this.running.forEach(({ reject }) => reject(error)); // Reject all queued requests this.running = []; // Clear the running queue } - }; + } + const delay = (time, isRefreshToken = false) => { if (!this.paused) { diff --git a/lib/core/oauthHandler.js b/lib/core/oauthHandler.js index 699abdc8..69a0873c 100644 --- a/lib/core/oauthHandler.js +++ b/lib/core/oauthHandler.js @@ -89,6 +89,9 @@ export default class OAuthHandler { */ async authorize() { try { + if (!this.OAuthBaseURL) { + throw new Error('OAuthBaseURL is not set'); + } const baseUrl = `${this.OAuthBaseURL}/#!/apps/${this.appId}/authorize`; const authUrl = new URL(baseUrl); authUrl.searchParams.set('response_type', 'code'); // Using set() to avoid duplicate parameters diff --git a/types/oauthHandler.d.ts b/types/oauthHandler.d.ts index 7d319347..0a16bf29 100644 --- a/types/oauthHandler.d.ts +++ b/types/oauthHandler.d.ts @@ -5,11 +5,11 @@ interface OAuthResponse { expires_in: number; organization_uid: string; user_uid: string; - token_type: string, - location: string, - region: string, - authorization_type: string, - stack_api_key: string + token_type: string; + location: string; + region: string; + authorization_type: string; + stack_api_key: string; } export default class OAuthHandler { @@ -45,6 +45,60 @@ export default class OAuthHandler { */ getAccessToken(): string; + /** + * Get the current refresh token + * @returns The refresh token + */ + getRefreshToken(): string; + + /** + * Get the current organization UID + * @returns The organization UID + */ + getOrganizationUID(): string; + + /** + * Get the current user UID + * @returns The user UID + */ + getUserUID(): string; + + /** + * Get the token expiry time + * @returns The token expiry time + */ + getTokenExpiryTime(): string; + + /** + * Set the access token + * @param token - The access token + */ + setAccessToken(token: string): void; + + /** + * Set the refresh token + * @param token - The refresh token + */ + setRefreshToken(token: string): void; + + /** + * Set organization UID + * @param organizationUID - The organization UID + */ + setOrganizationUID(organizationUID: string): void; + + /** + * Set user UID + * @param userUID - The user UID + */ + setUserUID(userUID: string): void; + + /** + * Set expiry time + * @param expiryTime - The expiry time + */ + setTokenExpiryTime(expiryTime: Date): void; + /** * Handle the OAuth redirect URL and exchange the authorization code for a token * @param url - The redirect URL containing the authorization code From 682b5bd2e7e7503bc6b792f74783ada8405e00bc Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Tue, 11 Mar 2025 22:52:57 +0530 Subject: [PATCH 05/12] feat: oauth unit test cases --- test/unit/index.js | 1 + test/unit/oauthHandler-test.js | 325 +++++++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+) create mode 100644 test/unit/oauthHandler-test.js diff --git a/test/unit/index.js b/test/unit/index.js index a49ffca1..6cafbd0c 100644 --- a/test/unit/index.js +++ b/test/unit/index.js @@ -44,3 +44,4 @@ require('./variantGroup-test') require('./ungroupedVariants-test') require('./variantsWithVariantsGroup-test') require('./variants-entry-test') +require('./oauthHandler-test') diff --git a/test/unit/oauthHandler-test.js b/test/unit/oauthHandler-test.js new file mode 100644 index 00000000..f74a8da0 --- /dev/null +++ b/test/unit/oauthHandler-test.js @@ -0,0 +1,325 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import axios from 'axios'; +import OAuthHandler from '../../lib/core/oauthHandler'; + +describe('OAuthHandler', () => { + let axiosInstance; + let oauthHandler; + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + axiosInstance = axios.create({ + uiBaseUrl: 'https://example.com', // Make sure this is correctly set + developerHubBaseUrl: 'https://developer.example.com', + baseURL: 'https://api.example.com', + }); + oauthHandler = new OAuthHandler(axiosInstance, 'appId', 'clientId', 'http://localhost:8184', 'clientSecret'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should initialize OAuthHandler with correct properties', () => { + expect(oauthHandler.appId).to.equal('appId'); + expect(oauthHandler.clientId).to.equal('clientId'); + expect(oauthHandler.redirectUri).to.equal('http://localhost:8184'); + expect(oauthHandler.responseType).to.equal('code'); + expect(oauthHandler.scope).to.equal(''); + expect(oauthHandler.clientSecret).to.equal('clientSecret'); + expect(oauthHandler.OAuthBaseURL).to.equal('https://example.com'); + expect(oauthHandler.axiosInstance).to.equal(axiosInstance); + }); + + it('should generate code verifier', () => { + const codeVerifier = oauthHandler.generateCodeVerifier(); + expect(codeVerifier).to.have.lengthOf(128); + }); + + it('should generate code challenge', async () => { + const codeVerifier = 'testCodeVerifier'; + const codeChallenge = await oauthHandler.generateCodeChallenge(codeVerifier); + expect(codeChallenge).to.exist; + }); + + it('should authorize and return authorization URL', async () => { + const authUrl = await oauthHandler.authorize(); + expect(authUrl).to.include('https://example.com/'); + expect(authUrl).to.include('response_type=code'); + expect(authUrl).to.include('client_id=clientId'); + }); + + it('should exchange code for token', async () => { + const tokenData = { access_token: 'accessToken', refresh_token: 'refreshToken', expires_in: 3600 }; + sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }); + + const result = await oauthHandler.exchangeCodeForToken('authorization_code'); + expect(result).to.deep.equal(tokenData); + expect(result).to.include('data'); + }); + + it('should refresh access token', async () => { + const tokenData = { access_token: 'newAccessToken', refresh_token: 'newRefreshToken', expires_in: 3600 }; + sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }); + + const result = await oauthHandler.refreshAccessToken('refreshToken'); + expect(result).to.deep.equal(tokenData); + }); + + it('should logout successfully', async () => { + sandbox.stub(oauthHandler, 'getOauthAppAuthorization').resolves('authorizationId'); + sandbox.stub(oauthHandler, 'revokeOauthAppAuthorization').resolves({}); + + const result = await oauthHandler.logout(); + expect(result).to.equal('Logged out successfully'); + }); + + it('should handle redirect and exchange code for token', async () => { + const exchangeStub = sandbox.stub(oauthHandler, 'exchangeCodeForToken').resolves({}); + + await oauthHandler.handleRedirect('http://localhost:8184?code=authorization_code'); + expect(exchangeStub.calledWith('authorization_code')).to.be.true; + }); + + it('should get access token', () => { + oauthHandler.axiosInstance.oauth = { accessToken: 'accessToken' }; + const accessToken = oauthHandler.getAccessToken(); + expect(accessToken).to.equal('accessToken'); + }); + + it('should get refresh token', () => { + oauthHandler.axiosInstance.oauth = { refreshToken: 'refreshToken' }; + const refreshToken = oauthHandler.getRefreshToken(); + expect(refreshToken).to.equal('refreshToken'); + }); + + it('should get organization UID', () => { + oauthHandler.axiosInstance.oauth = { organizationUID: 'organizationUID' }; + const organizationUID = oauthHandler.getOrganizationUID(); + expect(organizationUID).to.equal('organizationUID'); + }); + + it('should get user UID', () => { + oauthHandler.axiosInstance.oauth = { userUID: 'userUID' }; + const userUID = oauthHandler.getUserUID(); + expect(userUID).to.equal('userUID'); + }); + + it('should get token expiry time', () => { + oauthHandler.axiosInstance.oauth = { tokenExpiryTime: 1234567890 }; + const tokenExpiryTime = oauthHandler.getTokenExpiryTime(); + expect(tokenExpiryTime).to.equal(1234567890); + }); + + it('should set access token', () => { + oauthHandler.setAccessToken('newAccessToken'); + expect(oauthHandler.axiosInstance.oauth.accessToken).to.equal('newAccessToken'); + }); + + it('should set refresh token', () => { + oauthHandler.setRefreshToken('newRefreshToken'); + expect(oauthHandler.axiosInstance.oauth.refreshToken).to.equal('newRefreshToken'); + }); + + it('should set organization UID', () => { + oauthHandler.setOrganizationUID('newOrganizationUID'); + expect(oauthHandler.axiosInstance.oauth.organizationUID).to.equal('newOrganizationUID'); + }); + + it('should set user UID', () => { + oauthHandler.setUserUID('newUserUID'); + expect(oauthHandler.axiosInstance.oauth.userUID).to.equal('newUserUID'); + }); + + it('should set token expiry time', () => { + oauthHandler.setTokenExpiryTime(1234567890); + expect(oauthHandler.axiosInstance.oauth.tokenExpiryTime).to.equal(1234567890); + }); + + it('should generate codeVerifier and set codeChallenge to null if clientSecret is not provided', () => { + const oauthHandlerWithoutClientSecret = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + null, // No clientSecret + ); + + // Ensure codeVerifier is generated + expect(oauthHandlerWithoutClientSecret.codeVerifier).to.exist; + + // Ensure codeChallenge is null initially + expect(oauthHandlerWithoutClientSecret.codeChallenge).to.equal(null); + }); + + it('should not generate codeVerifier or codeChallenge if clientSecret is provided', () => { + const oauthHandlerWithClientSecret = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + 'clientSecret', // clientSecret is provided + ); + + // codeVerifier and codeChallenge should not be set if clientSecret is provided + expect(oauthHandlerWithClientSecret.codeVerifier).to.equal(undefined); + expect(oauthHandlerWithClientSecret.codeChallenge).to.equal(undefined); + }); + + it('should generate codeChallenge after calling generateCodeChallenge when clientSecret is not provided', async () => { + const oauthHandlerWithoutClientSecret = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + null, // No clientSecret + ); + + const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier; + expect(codeVerifier).to.exist; // Ensure codeVerifier is generated + + const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier); + + // Ensure the codeChallenge is a URL-safe Base64 string + expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/); // URL-safe Base64 + }); + + it('should use the Web Crypto API in a browser environment', async () => { + // Mock the browser environment + global.window = { + crypto: { + subtle: { + digest: sinon.stub().resolves(new Uint8Array([1, 2, 3, 4])), // Mock hash result + }, + }, + }; + + const oauthHandlerWithoutClientSecret = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + null, // No clientSecret + ); + + const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier; + expect(codeVerifier).to.exist; // Ensure codeVerifier is generated + + const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier); + + // Ensure the codeChallenge is a URL-safe Base64 string + expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/); // URL-safe Base64 + + // Clean up after the test to avoid affecting other tests + delete global.window; + }); + + it('should generate authorization URL with code_challenge when clientSecret is not provided', async () => { + // Mock OAuthHandler without clientSecret + oauthHandler = new OAuthHandler( + axiosInstance, + 'appId', + 'clientId', + 'http://localhost:8184', + null, // No clientSecret (PKCE) + ); + + // Stub the generateCodeChallenge to return a dummy value + const codeChallenge = 'dummyCodeChallenge'; + sandbox.stub(oauthHandler, 'generateCodeChallenge').resolves(codeChallenge); + + const authUrl = await oauthHandler.authorize(); + + // Check that code_challenge and code_challenge_method are included in the URL + expect(authUrl).to.include('https://example.com/'); + expect(authUrl).to.include('response_type=code'); + expect(authUrl).to.include('client_id=clientId'); + expect(authUrl).to.include('code_challenge=dummyCodeChallenge'); + expect(authUrl).to.include('code_challenge_method=S256'); + }); + + // Test cases for getOauthAppAuthorization + describe('getOauthAppAuthorization', () => { + it('should return authorization_uid when authorizations exist for the current user', async () => { + const mockResponse = { + data: { + data: [ + { + user: { uid: 'currentUserUid' }, + authorization_uid: 'authorizationUid1', + }, + ], + }, + }; + + sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + + oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid'; + const authorizationUid = await oauthHandler.getOauthAppAuthorization(); + + expect(authorizationUid).to.equal('authorizationUid1'); + }); + + it('should throw an error when no authorizations found for the current user', async () => { + const mockResponse = { + data: { + data: [ + { + user: { uid: 'otherUserUid' }, + authorization_uid: 'authorizationUid2', + }, + ], + }, + }; + + sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + + oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid'; + + try { + await oauthHandler.getOauthAppAuthorization(); + throw new Error('Expected error not thrown'); + } catch (error) { + expect(error.message).to.equal('No authorizations found for current user!'); + } + }); + + it('should throw an error when no authorizations found for the app', async () => { + const mockResponse = { data: { data: [] } }; + + sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + + try { + await oauthHandler.getOauthAppAuthorization(); + throw new Error('Expected error not thrown'); + } catch (error) { + expect(error.message).to.equal('No authorizations found for the app!'); + } + }); + }); + + describe('revokeOauthAppAuthorization', () => { + it('should make a DELETE request to revoke authorization when valid authorizationId is provided', async () => { + const authorizationId = 'authorizationUid1'; + const mockResponse = { data: { success: true } }; + + sandbox.stub(axiosInstance, 'delete').resolves(mockResponse); + + const result = await oauthHandler.revokeOauthAppAuthorization(authorizationId); + + expect(result.success).to.be.true; + expect(axiosInstance.delete.calledOnce).to.be.true; + }); + + it('should not make a DELETE request when authorizationId is invalid or empty', async () => { + const invalidAuthorizationId = ''; + const deleteStub = sandbox.stub(axiosInstance, 'delete'); + + await oauthHandler.revokeOauthAppAuthorization(invalidAuthorizationId); + + expect(deleteStub.called).to.be.false; + }); + }); +}); From 2f411ae35c0f3397d372ea63fe7c574f72b18bb0 Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Thu, 13 Mar 2025 12:17:02 +0530 Subject: [PATCH 06/12] fix: lint issues --- lib/contentstackClient.js | 20 +-- lib/core/concurrency-queue.js | 35 ++-- lib/core/contentstackHTTPClient.js | 12 +- lib/core/oauthHandler.js | 266 ++++++++++++++--------------- 4 files changed, 166 insertions(+), 167 deletions(-) diff --git a/lib/contentstackClient.js b/lib/contentstackClient.js index 4ae95723..830be9b6 100644 --- a/lib/contentstackClient.js +++ b/lib/contentstackClient.js @@ -193,15 +193,15 @@ export default function contentstackClient ({ http }) { * .then(() => console.log('Logged in successfully')) * */ - function oauth(params = {}) { - http.defaults.versioningStrategy = "path"; - const appId = params.appId || '6400aa06db64de001a31c8a9'; - const clientId = params.clientId || 'Ie0FEfTzlfAHL4xM'; - const redirectUri = params.redirectUri || 'http://localhost:8184'; - const responseType = params.responseType || 'code'; - const scope = params.scope; - const clientSecret = params.clientSecret; - return new OAuthHandler(http, appId, clientId, redirectUri, clientSecret,responseType, scope); + function oauth (params = {}) { + http.defaults.versioningStrategy = 'path' + const appId = params.appId || '6400aa06db64de001a31c8a9' + const clientId = params.clientId || 'Ie0FEfTzlfAHL4xM' + const redirectUri = params.redirectUri || 'http://localhost:8184' + const responseType = params.responseType || 'code' + const scope = params.scope + const clientSecret = params.clientSecret + return new OAuthHandler(http, appId, clientId, redirectUri, clientSecret, responseType, scope) } return { @@ -211,6 +211,6 @@ export default function contentstackClient ({ http }) { stack: stack, organization: organization, axiosInstance: http, - oauth, + oauth } } diff --git a/lib/core/concurrency-queue.js b/lib/core/concurrency-queue.js index 779b6d93..868307e3 100644 --- a/lib/core/concurrency-queue.js +++ b/lib/core/concurrency-queue.js @@ -77,16 +77,16 @@ export function ConcurrencyQueue ({ axios, config }) { request.data = transformFormData(request) } if (axios?.oauth?.accessToken) { - const isTokenExpired = axios.oauth.tokenExpiryTime && Date.now() > axios.oauth.tokenExpiryTime; + const isTokenExpired = axios.oauth.tokenExpiryTime && Date.now() > axios.oauth.tokenExpiryTime if (isTokenExpired) { return refreshAccessToken().catch((error) => { - throw new Error('Failed to refresh access token: ' + error.message); - }); + throw new Error('Failed to refresh access token: ' + error.message) + }) } - } - + } + request.retryCount = request?.retryCount || 0 - setAuthorizationHeaders(request); + setAuthorizationHeaders(request) if (request.cancelToken === undefined) { const source = Axios.CancelToken.source() request.cancelToken = source.token @@ -121,32 +121,31 @@ export function ConcurrencyQueue ({ axios, config }) { request.authtoken = this.config.authtoken } else if (axios?.oauth?.accessToken) { // If OAuth access token is available in axios instance - request.headers.authorization = `Bearer ${axios.oauth.accessToken}`; - request.authorization = `Bearer ${axios.oauth.accessToken}`; + request.headers.authorization = `Bearer ${axios.oauth.accessToken}` + request.authorization = `Bearer ${axios.oauth.accessToken}` delete request.headers.authtoken } } - //Refresh Access Token + // Refresh Access Token const refreshAccessToken = async () => { try { // Try to refresh the token - await new OAuthHandler(axios).refreshAccessToken(); - this.paused = false; // Resume the request queue once the token is refreshed + await new OAuthHandler(axios).refreshAccessToken() + this.paused = false // Resume the request queue once the token is refreshed // Retry the requests that were pending due to token expiration this.running.forEach(({ request, resolve, reject }) => { // Retry the request - axios(request).then(resolve).catch(reject); - }); - this.running = []; // Clear the running queue after retrying requests + axios(request).then(resolve).catch(reject) + }) + this.running = [] // Clear the running queue after retrying requests } catch (error) { - this.paused = false; // stop queueing requests on failure - this.running.forEach(({ reject }) => reject(error)); // Reject all queued requests - this.running = []; // Clear the running queue + this.paused = false // stop queueing requests on failure + this.running.forEach(({ reject }) => reject(error)) // Reject all queued requests + this.running = [] // Clear the running queue } } - const delay = (time, isRefreshToken = false) => { if (!this.paused) { diff --git a/lib/core/contentstackHTTPClient.js b/lib/core/contentstackHTTPClient.js index f63f6e3c..02f590d5 100644 --- a/lib/core/contentstackHTTPClient.js +++ b/lib/core/contentstackHTTPClient.js @@ -65,24 +65,24 @@ export default function contentstackHttpClient (options) { config.basePath = `/${config.basePath.split('/').filter(Boolean).join('/')}` } const baseURL = config.endpoint || `${protocol}://${hostname}:${port}${config.basePath}/{api-version}` - let uiHostName = hostname; - let developerHubBaseUrl = hostname; + let uiHostName = hostname + let developerHubBaseUrl = hostname if (hostname.endsWith('io')) { - uiHostName = hostname.replace('io', 'com'); + uiHostName = hostname.replace('io', 'com') } if (hostname.startsWith('api')) { - uiHostName = uiHostName.replace('api', 'app'); + uiHostName = uiHostName.replace('api', 'app') } - const uiBaseUrl = config.endpoint || `${protocol}://${uiHostName}`; + const uiBaseUrl = config.endpoint || `${protocol}://${uiHostName}` developerHubBaseUrl = developerHubBaseUrl ?.replace('api', 'developerhub-api') .replace(/^dev\d+/, 'dev') // Replaces any 'dev1', 'dev2', etc. with 'dev' .replace('io', 'com') .replace(/^http/, '') // Removing `http` if already present - .replace(/^/, 'https://'); // Adds 'https://' at the start if not already there + .replace(/^/, 'https://') // Adds 'https://' at the start if not already there // set ui host name const axiosOptions = { diff --git a/lib/core/oauthHandler.js b/lib/core/oauthHandler.js index 69a0873c..3a42edb6 100644 --- a/lib/core/oauthHandler.js +++ b/lib/core/oauthHandler.js @@ -1,4 +1,4 @@ -import errorFormatter from './contentstackError'; +import errorFormatter from './contentstackError' /** * @description OAuthHandler class to handle OAuth authorization and token management @@ -17,62 +17,62 @@ import errorFormatter from './contentstackError'; * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); */ export default class OAuthHandler { - constructor(axiosInstance, appId, clientId, redirectUri, clientSecret, responseType = 'code', scope = []) { - this.appId = appId; - this.clientId = clientId; - this.redirectUri = redirectUri; - this.responseType = responseType; - this.scope = scope.join(' '); - this.clientSecret = clientSecret; // Optional, if provided, PKCE will be skipped - this.OAuthBaseURL = axiosInstance.defaults.uiBaseUrl; - this.axiosInstance = axiosInstance; - this.axiosInstance.oauth = axiosInstance?.oauth || {}; - this.axiosInstance.oauth.redirectUri = redirectUri; - this.axiosInstance.oauth.clientId = clientId; - this.axiosInstance.oauth.appId = appId; - this.developerHubBaseUrl = axiosInstance.defaults.developerHubBaseUrl; + constructor (axiosInstance, appId, clientId, redirectUri, clientSecret, responseType = 'code', scope = []) { + this.appId = appId + this.clientId = clientId + this.redirectUri = redirectUri + this.responseType = responseType + this.scope = scope.join(' ') + this.clientSecret = clientSecret // Optional, if provided, PKCE will be skipped + this.OAuthBaseURL = axiosInstance.defaults.uiBaseUrl + this.axiosInstance = axiosInstance + this.axiosInstance.oauth = axiosInstance?.oauth || {} + this.axiosInstance.oauth.redirectUri = redirectUri + this.axiosInstance.oauth.clientId = clientId + this.axiosInstance.oauth.appId = appId + this.developerHubBaseUrl = axiosInstance.defaults.developerHubBaseUrl // Only generate PKCE codeVerifier and codeChallenge if clientSecret is not provided if (!this.clientSecret) { - this.codeVerifier = this.generateCodeVerifier(); - this.codeChallenge = null; + this.codeVerifier = this.generateCodeVerifier() + this.codeChallenge = null } } // Helper function for setting common headers for API requests - _getHeaders() { + _getHeaders () { return { - 'Content-Type': 'application/x-www-form-urlencoded', - }; + 'Content-Type': 'application/x-www-form-urlencoded' + } } // Generate a random string (code_verifier) - generateCodeVerifier(length = 128) { - const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; - return Array.from({ length }, () => charset.charAt(Math.floor(Math.random() * charset.length))).join(''); + generateCodeVerifier (length = 128) { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' + return Array.from({ length }, () => charset.charAt(Math.floor(Math.random() * charset.length))).join('') } - async generateCodeChallenge(codeVerifier) { + async generateCodeChallenge (codeVerifier) { // Check if in a browser environment or Node.js if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) { // Use the native Web Crypto API in the browser - const encoder = new TextEncoder(); - const data = encoder.encode(codeVerifier); - const hashBuffer = await window.crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const base64String = btoa(String.fromCharCode(...hashArray)); - return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe Base64 + const encoder = new TextEncoder() + const data = encoder.encode(codeVerifier) + const hashBuffer = await window.crypto.subtle.digest('SHA-256', data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + const base64String = btoa(String.fromCharCode(...hashArray)) + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') // URL-safe Base64 } else { // In Node.js: Use the native `crypto` module for hashing - const crypto = require('crypto'); + const crypto = require('crypto') - const hash = crypto.createHash('sha256'); - hash.update(codeVerifier); - const hashBuffer = hash.digest(); + const hash = crypto.createHash('sha256') + hash.update(codeVerifier) + const hashBuffer = hash.digest() // Convert to Base64 and URL-safe Base64 - let base64String = hashBuffer.toString('base64'); - return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); // URL-safe Base64 + const base64String = hashBuffer.toString('base64') + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') // URL-safe Base64 } } @@ -87,26 +87,26 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const authUrl = await oauthHandler.authorize(); */ - async authorize() { + async authorize () { try { if (!this.OAuthBaseURL) { - throw new Error('OAuthBaseURL is not set'); + throw new Error('OAuthBaseURL is not set') } - const baseUrl = `${this.OAuthBaseURL}/#!/apps/${this.appId}/authorize`; - const authUrl = new URL(baseUrl); - authUrl.searchParams.set('response_type', 'code'); // Using set() to avoid duplicate parameters - authUrl.searchParams.set('client_id', this.clientId); + const baseUrl = `${this.OAuthBaseURL}/#!/apps/${this.appId}/authorize` + const authUrl = new URL(baseUrl) + authUrl.searchParams.set('response_type', 'code') // Using set() to avoid duplicate parameters + authUrl.searchParams.set('client_id', this.clientId) if (this.clientSecret) { - return authUrl.toString(); + return authUrl.toString() } else { // PKCE flow: add code_challenge to the authorization URL - this.codeChallenge = await this.generateCodeChallenge(this.codeVerifier); - authUrl.searchParams.set('code_challenge', this.codeChallenge); - authUrl.searchParams.set('code_challenge_method', 'S256'); - return authUrl.toString(); + this.codeChallenge = await this.generateCodeChallenge(this.codeVerifier) + authUrl.searchParams.set('code_challenge', this.codeChallenge) + authUrl.searchParams.set('code_challenge_method', 'S256') + return authUrl.toString() } } catch (error) { - errorFormatter(error); + errorFormatter(error) } } @@ -122,33 +122,33 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const tokenData = await oauthHandler.exchangeCodeForToken('authorization_code'); */ - async exchangeCodeForToken(code) { + async exchangeCodeForToken (code) { const body = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: this.redirectUri, client_id: this.clientId, - ...(this.clientSecret ? { client_secret: this.clientSecret } : { code_verifier: this.codeVerifier }), // Choose between client_secret and code_verifier - }); + ...(this.clientSecret ? { client_secret: this.clientSecret } : { code_verifier: this.codeVerifier }) // Choose between client_secret and code_verifier + }) - this.axiosInstance.defaults.headers = this._getHeaders(); + this.axiosInstance.defaults.headers = this._getHeaders() try { - const response = await this.axiosInstance.post(`${this.OAuthBaseURL}/apps-api/apps/token`, body); + const response = await this.axiosInstance.post(`${this.OAuthBaseURL}/apps-api/apps/token`, body) - this._saveTokens(response.data); - return response.data; + this._saveTokens(response.data) + return response.data } catch (error) { - errorFormatter(error); + errorFormatter(error) } } // Save tokens and token expiry details - _saveTokens(data) { - this.axiosInstance.oauth.accessToken = data.access_token; - this.axiosInstance.oauth.refreshToken = data.refresh_token || this.axiosInstance.oauth.refreshToken; - this.axiosInstance.oauth.organizationUID = data.organization_uid; - this.axiosInstance.oauth.userUID = data.user_uid; - this.axiosInstance.oauth.tokenExpiryTime = Date.now() + (data.expires_in - 60) * 1000; // Store expiry time + _saveTokens (data) { + this.axiosInstance.oauth.accessToken = data.access_token + this.axiosInstance.oauth.refreshToken = data.refresh_token || this.axiosInstance.oauth.refreshToken + this.axiosInstance.oauth.organizationUID = data.organization_uid + this.axiosInstance.oauth.userUID = data.user_uid + this.axiosInstance.oauth.tokenExpiryTime = Date.now() + (data.expires_in - 60) * 1000 // Store expiry time } /** @@ -164,31 +164,31 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const tokenData = await oauthHandler.refreshAccessToken(); */ - async refreshAccessToken(providedRefreshToken = null) { - const refreshToken = providedRefreshToken || this.axiosInstance.oauth.refreshToken; + async refreshAccessToken (providedRefreshToken = null) { + const refreshToken = providedRefreshToken || this.axiosInstance.oauth.refreshToken if (!refreshToken) { - throw new Error('No refresh token available. Please authenticate first.'); + throw new Error('No refresh token available. Please authenticate first.') } const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: this.axiosInstance.oauth.clientId, - redirect_uri: this.axiosInstance.oauth.redirectUri, - }); + redirect_uri: this.axiosInstance.oauth.redirectUri + }) - this.axiosInstance.defaults.headers = this._getHeaders(); + this.axiosInstance.defaults.headers = this._getHeaders() try { - const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/apps/token`, body); + const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/apps/token`, body) - const data = response.data; - this.axiosInstance.oauth.accessToken = data.access_token; - this.axiosInstance.oauth.refreshToken = data.refresh_token || this.axiosInstance.oauth.refreshToken; // Optionally update refresh token - this.axiosInstance.oauth.tokenExpiryTime = Date.now() + (data.expires_in - 60) * 1000; // Update expiry time - return data; + const data = response.data + this.axiosInstance.oauth.accessToken = data.access_token + this.axiosInstance.oauth.refreshToken = data.refresh_token || this.axiosInstance.oauth.refreshToken // Optionally update refresh token + this.axiosInstance.oauth.tokenExpiryTime = Date.now() + (data.expires_in - 60) * 1000 // Update expiry time + return data } catch (error) { - errorFormatter(error); + errorFormatter(error) } } @@ -204,14 +204,14 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const resp = await oauthHandler.logout(); */ - async logout() { + async logout () { try { - const authorizationId = await this.getOauthAppAuthorization(); - await this.revokeOauthAppAuthorization(authorizationId); - this.axiosInstance.oauth = {}; // Clear OAuth data - return 'Logged out successfully'; + const authorizationId = await this.getOauthAppAuthorization() + await this.revokeOauthAppAuthorization(authorizationId) + this.axiosInstance.oauth = {} // Clear OAuth data + return 'Logged out successfully' } catch (error) { - errorFormatter(error); + errorFormatter(error) } } @@ -226,8 +226,8 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const accessToken = oauthHandler.getAccessToken(); */ - getAccessToken() { - return this.axiosInstance.oauth.accessToken; + getAccessToken () { + return this.axiosInstance.oauth.accessToken } /** @@ -241,8 +241,8 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const refreshToken = oauthHandler.getRefreshToken(); */ - getRefreshToken() { - return this.axiosInstance.oauth.refreshToken || null; + getRefreshToken () { + return this.axiosInstance.oauth.refreshToken || null } /** @@ -256,8 +256,8 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const orgUID = oauthHandler.getOrganizationUID(); */ - getOrganizationUID() { - return this.axiosInstance.oauth.organizationUID || null; + getOrganizationUID () { + return this.axiosInstance.oauth.organizationUID || null } /** @@ -271,8 +271,8 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const userId = oauthHandler.getUserUID(); */ - getUserUID() { - return this.axiosInstance.oauth.userUID || null; + getUserUID () { + return this.axiosInstance.oauth.userUID || null } /** @@ -286,8 +286,8 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * const expiryTime = oauthHandler.getTokenExpiryTime(); */ - getTokenExpiryTime() { - return this.axiosInstance.oauth.tokenExpiryTime || null; + getTokenExpiryTime () { + return this.axiosInstance.oauth.tokenExpiryTime || null } /** @@ -301,11 +301,11 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * oauthHandler.setAccessToken('accessToken'); */ - setAccessToken(token) { + setAccessToken (token) { if (!token) { - throw new Error('Access token is required'); + throw new Error('Access token is required') } - this.axiosInstance.oauth.accessToken = token; + this.axiosInstance.oauth.accessToken = token } /** @@ -319,11 +319,11 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * oauthHandler.setRefreshToken('refreshToken'); */ - setRefreshToken(token) { + setRefreshToken (token) { if (!token) { - throw new Error('Refresh token is required'); + throw new Error('Refresh token is required') } - this.axiosInstance.oauth.refreshToken = token; + this.axiosInstance.oauth.refreshToken = token } /** @@ -337,11 +337,11 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * oauthHandler.setOrganizationUID('organizationUID'); */ - setOrganizationUID(organizationUID) { + setOrganizationUID (organizationUID) { if (!organizationUID) { - throw new Error('Organization UID is required'); + throw new Error('Organization UID is required') } - this.axiosInstance.oauth.organizationUID = organizationUID; + this.axiosInstance.oauth.organizationUID = organizationUID } /** @@ -355,11 +355,11 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * oauthHandler.setUserUID('userID'); */ - setUserUID(userUID) { + setUserUID (userUID) { if (!userUID) { - throw new Error('User UID is required'); + throw new Error('User UID is required') } - this.axiosInstance.oauth.userUID = userUID; + this.axiosInstance.oauth.userUID = userUID } /** @@ -373,11 +373,11 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * oauthHandler.setTokenExpiryTime('expiryTime'); // date format */ - setTokenExpiryTime(expiryTime) { + setTokenExpiryTime (expiryTime) { if (!expiryTime) { - throw new Error('Token expiry time is required'); + throw new Error('Token expiry time is required') } - this.axiosInstance.oauth.tokenExpiryTime = expiryTime; + this.axiosInstance.oauth.tokenExpiryTime = expiryTime } /** @@ -394,18 +394,18 @@ export default class OAuthHandler { * const oauthHandler = client.oauth({ appId: 'appId', clientId: 'clientId', redirectUri: 'http://localhost:8184' }); * await oauthHandler.handleRedirect('http://localhost:8184?code=authorization_code'); */ - async handleRedirect(url) { - const urlParams = new URLSearchParams(new URL(url).search); - const code = urlParams.get('code'); + async handleRedirect (url) { + const urlParams = new URLSearchParams(new URL(url).search) + const code = urlParams.get('code') if (code) { try { - await this.exchangeCodeForToken(code); + await this.exchangeCodeForToken(code) } catch (error) { - errorFormatter(error); + errorFormatter(error) } } else { - throw new Error('Authorization code not found in redirect URL.'); + throw new Error('Authorization code not found in redirect URL.') } } @@ -415,32 +415,32 @@ export default class OAuthHandler { * @func getOauthAppAuthorization * @returns {any} */ - async getOauthAppAuthorization() { + async getOauthAppAuthorization () { const headers = { authorization: `Bearer ${this.axiosInstance.oauth.accessToken}`, organization_uid: this.axiosInstance.oauth.organizationUID, - 'Content-type': 'application/json', - }; + 'Content-type': 'application/json' + } - this.axiosInstance.defaults.headers = headers; + this.axiosInstance.defaults.headers = headers try { const res = await this.axiosInstance.get( - `${this.developerHubBaseUrl}/manifests/${this.axiosInstance.oauth.appId}/authorizations`, - ); + `${this.developerHubBaseUrl}/manifests/${this.axiosInstance.oauth.appId}/authorizations` + ) - const data = res.data; + const data = res.data if (data?.data?.length > 0) { - const userUid = this.axiosInstance.oauth.userUID; - const currentUserAuthorization = data?.data?.filter((element) => element.user.uid === userUid) || []; + const userUid = this.axiosInstance.oauth.userUID + const currentUserAuthorization = data?.data?.filter((element) => element.user.uid === userUid) || [] if (currentUserAuthorization.length === 0) { - throw new Error('No authorizations found for current user!'); + throw new Error('No authorizations found for current user!') } - return currentUserAuthorization[0].authorization_uid; // filter authorizations by current logged in user + return currentUserAuthorization[0].authorization_uid // filter authorizations by current logged in user } else { - throw new Error('No authorizations found for the app!'); + throw new Error('No authorizations found for the app!') } } catch (error) { - errorFormatter(error); + errorFormatter(error) } } @@ -451,23 +451,23 @@ export default class OAuthHandler { * @param {any} authorizationId * @returns {any} */ - async revokeOauthAppAuthorization(authorizationId) { + async revokeOauthAppAuthorization (authorizationId) { if (authorizationId?.length > 1) { const headers = { authorization: `Bearer ${this.axiosInstance.oauth.accessToken}`, organization_uid: this.axiosInstance.oauth.organizationUID, - 'Content-type': 'application/json', - }; + 'Content-type': 'application/json' + } - this.axiosInstance.defaults.headers = headers; + this.axiosInstance.defaults.headers = headers try { const res = await this.axiosInstance.delete( - `${this.developerHubBaseUrl}/manifests/${this.axiosInstance.oauth.appId}/authorizations/${authorizationId}`, - ); + `${this.developerHubBaseUrl}/manifests/${this.axiosInstance.oauth.appId}/authorizations/${authorizationId}` + ) - return res.data; + return res.data } catch (error) { - errorFormatter(error); + errorFormatter(error) } } } From 6fa265a43b1859834b0afd062616e019d507c20c Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Thu, 13 Mar 2025 16:32:17 +0530 Subject: [PATCH 07/12] fix: oauth unit test cases linting issues --- test/unit/oauthHandler-test.js | 357 +++++++++++++++++---------------- 1 file changed, 182 insertions(+), 175 deletions(-) diff --git a/test/unit/oauthHandler-test.js b/test/unit/oauthHandler-test.js index f74a8da0..dae925e1 100644 --- a/test/unit/oauthHandler-test.js +++ b/test/unit/oauthHandler-test.js @@ -1,142 +1,145 @@ -import { expect } from 'chai'; -import sinon from 'sinon'; -import axios from 'axios'; -import OAuthHandler from '../../lib/core/oauthHandler'; +import { expect } from 'chai' +import sinon from 'sinon' +import axios from 'axios' +import OAuthHandler from '../../lib/core/oauthHandler' +import { describe, it, beforeEach, afterEach } from 'mocha' describe('OAuthHandler', () => { - let axiosInstance; - let oauthHandler; - let sandbox; + let axiosInstance + let oauthHandler + let sandbox beforeEach(() => { - sandbox = sinon.createSandbox(); + sandbox = sinon.createSandbox() axiosInstance = axios.create({ uiBaseUrl: 'https://example.com', // Make sure this is correctly set developerHubBaseUrl: 'https://developer.example.com', - baseURL: 'https://api.example.com', - }); - oauthHandler = new OAuthHandler(axiosInstance, 'appId', 'clientId', 'http://localhost:8184', 'clientSecret'); - }); + baseURL: 'https://api.example.com' + }) + oauthHandler = new OAuthHandler(axiosInstance, 'appId', 'clientId', 'http://localhost:8184', 'clientSecret') + }) afterEach(() => { - sandbox.restore(); - }); + sandbox.restore() + }) it('should initialize OAuthHandler with correct properties', () => { - expect(oauthHandler.appId).to.equal('appId'); - expect(oauthHandler.clientId).to.equal('clientId'); - expect(oauthHandler.redirectUri).to.equal('http://localhost:8184'); - expect(oauthHandler.responseType).to.equal('code'); - expect(oauthHandler.scope).to.equal(''); - expect(oauthHandler.clientSecret).to.equal('clientSecret'); - expect(oauthHandler.OAuthBaseURL).to.equal('https://example.com'); - expect(oauthHandler.axiosInstance).to.equal(axiosInstance); - }); + expect(oauthHandler.appId).to.equal('appId') + expect(oauthHandler.clientId).to.equal('clientId') + expect(oauthHandler.redirectUri).to.equal('http://localhost:8184') + expect(oauthHandler.responseType).to.equal('code') + expect(oauthHandler.scope).to.equal('') + expect(oauthHandler.clientSecret).to.equal('clientSecret') + expect(oauthHandler.OAuthBaseURL).to.equal('https://example.com') + expect(oauthHandler.axiosInstance).to.equal(axiosInstance) + }) it('should generate code verifier', () => { - const codeVerifier = oauthHandler.generateCodeVerifier(); - expect(codeVerifier).to.have.lengthOf(128); - }); + const codeVerifier = oauthHandler.generateCodeVerifier() + expect(codeVerifier).to.have.lengthOf(128) + }) it('should generate code challenge', async () => { - const codeVerifier = 'testCodeVerifier'; - const codeChallenge = await oauthHandler.generateCodeChallenge(codeVerifier); - expect(codeChallenge).to.exist; - }); + const codeVerifier = 'testCodeVerifier' + const codeChallenge = await oauthHandler.generateCodeChallenge(codeVerifier) + // eslint-disable-next-line no-unused-expressions + expect(codeChallenge).to.exist + }) it('should authorize and return authorization URL', async () => { - const authUrl = await oauthHandler.authorize(); - expect(authUrl).to.include('https://example.com/'); - expect(authUrl).to.include('response_type=code'); - expect(authUrl).to.include('client_id=clientId'); - }); + const authUrl = await oauthHandler.authorize() + expect(authUrl).to.include('https://example.com/') + expect(authUrl).to.include('response_type=code') + expect(authUrl).to.include('client_id=clientId') + }) it('should exchange code for token', async () => { - const tokenData = { access_token: 'accessToken', refresh_token: 'refreshToken', expires_in: 3600 }; - sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }); + const tokenData = { access_token: 'accessToken', refresh_token: 'refreshToken', expires_in: 3600 } + sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }) - const result = await oauthHandler.exchangeCodeForToken('authorization_code'); - expect(result).to.deep.equal(tokenData); - expect(result).to.include('data'); - }); + const result = await oauthHandler.exchangeCodeForToken('authorization_code') + expect(result).to.deep.equal(tokenData) + expect(result).to.include('data') + }) it('should refresh access token', async () => { - const tokenData = { access_token: 'newAccessToken', refresh_token: 'newRefreshToken', expires_in: 3600 }; - sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }); + const tokenData = { access_token: 'newAccessToken', refresh_token: 'newRefreshToken', expires_in: 3600 } + sandbox.stub(axiosInstance, 'post').resolves({ data: tokenData }) - const result = await oauthHandler.refreshAccessToken('refreshToken'); - expect(result).to.deep.equal(tokenData); - }); + const result = await oauthHandler.refreshAccessToken('refreshToken') + expect(result).to.deep.equal(tokenData) + }) it('should logout successfully', async () => { - sandbox.stub(oauthHandler, 'getOauthAppAuthorization').resolves('authorizationId'); - sandbox.stub(oauthHandler, 'revokeOauthAppAuthorization').resolves({}); + sandbox.stub(oauthHandler, 'getOauthAppAuthorization').resolves('authorizationId') + sandbox.stub(oauthHandler, 'revokeOauthAppAuthorization').resolves({}) - const result = await oauthHandler.logout(); - expect(result).to.equal('Logged out successfully'); - }); + const result = await oauthHandler.logout() + expect(result).to.equal('Logged out successfully') + }) it('should handle redirect and exchange code for token', async () => { - const exchangeStub = sandbox.stub(oauthHandler, 'exchangeCodeForToken').resolves({}); + const exchangeStub = sandbox.stub(oauthHandler, 'exchangeCodeForToken').resolves({}) - await oauthHandler.handleRedirect('http://localhost:8184?code=authorization_code'); - expect(exchangeStub.calledWith('authorization_code')).to.be.true; - }); + await oauthHandler.handleRedirect('http://localhost:8184?code=authorization_code') + // eslint-disable-next-line no-unused-expressions + expect(exchangeStub.calledWith('authorization_code')).to.be.true + }) it('should get access token', () => { - oauthHandler.axiosInstance.oauth = { accessToken: 'accessToken' }; - const accessToken = oauthHandler.getAccessToken(); - expect(accessToken).to.equal('accessToken'); - }); + oauthHandler.axiosInstance.oauth = { accessToken: 'accessToken' } + const accessToken = oauthHandler.getAccessToken() + expect(accessToken).to.equal('accessToken') + }) it('should get refresh token', () => { - oauthHandler.axiosInstance.oauth = { refreshToken: 'refreshToken' }; - const refreshToken = oauthHandler.getRefreshToken(); - expect(refreshToken).to.equal('refreshToken'); - }); + oauthHandler.axiosInstance.oauth = { refreshToken: 'refreshToken' } + const refreshToken = oauthHandler.getRefreshToken() + expect(refreshToken).to.equal('refreshToken') + }) it('should get organization UID', () => { - oauthHandler.axiosInstance.oauth = { organizationUID: 'organizationUID' }; - const organizationUID = oauthHandler.getOrganizationUID(); - expect(organizationUID).to.equal('organizationUID'); - }); + oauthHandler.axiosInstance.oauth = { organizationUID: 'organizationUID' } + const organizationUID = oauthHandler.getOrganizationUID() + expect(organizationUID).to.equal('organizationUID') + }) it('should get user UID', () => { - oauthHandler.axiosInstance.oauth = { userUID: 'userUID' }; - const userUID = oauthHandler.getUserUID(); - expect(userUID).to.equal('userUID'); - }); + oauthHandler.axiosInstance.oauth = { userUID: 'userUID' } + const userUID = oauthHandler.getUserUID() + expect(userUID).to.equal('userUID') + }) it('should get token expiry time', () => { - oauthHandler.axiosInstance.oauth = { tokenExpiryTime: 1234567890 }; - const tokenExpiryTime = oauthHandler.getTokenExpiryTime(); - expect(tokenExpiryTime).to.equal(1234567890); - }); + oauthHandler.axiosInstance.oauth = { tokenExpiryTime: 1234567890 } + const tokenExpiryTime = oauthHandler.getTokenExpiryTime() + expect(tokenExpiryTime).to.equal(1234567890) + }) it('should set access token', () => { - oauthHandler.setAccessToken('newAccessToken'); - expect(oauthHandler.axiosInstance.oauth.accessToken).to.equal('newAccessToken'); - }); + oauthHandler.setAccessToken('newAccessToken') + expect(oauthHandler.axiosInstance.oauth.accessToken).to.equal('newAccessToken') + }) it('should set refresh token', () => { - oauthHandler.setRefreshToken('newRefreshToken'); - expect(oauthHandler.axiosInstance.oauth.refreshToken).to.equal('newRefreshToken'); - }); + oauthHandler.setRefreshToken('newRefreshToken') + expect(oauthHandler.axiosInstance.oauth.refreshToken).to.equal('newRefreshToken') + }) it('should set organization UID', () => { - oauthHandler.setOrganizationUID('newOrganizationUID'); - expect(oauthHandler.axiosInstance.oauth.organizationUID).to.equal('newOrganizationUID'); - }); + oauthHandler.setOrganizationUID('newOrganizationUID') + expect(oauthHandler.axiosInstance.oauth.organizationUID).to.equal('newOrganizationUID') + }) it('should set user UID', () => { - oauthHandler.setUserUID('newUserUID'); - expect(oauthHandler.axiosInstance.oauth.userUID).to.equal('newUserUID'); - }); + oauthHandler.setUserUID('newUserUID') + expect(oauthHandler.axiosInstance.oauth.userUID).to.equal('newUserUID') + }) it('should set token expiry time', () => { - oauthHandler.setTokenExpiryTime(1234567890); - expect(oauthHandler.axiosInstance.oauth.tokenExpiryTime).to.equal(1234567890); - }); + oauthHandler.setTokenExpiryTime(1234567890) + expect(oauthHandler.axiosInstance.oauth.tokenExpiryTime).to.equal(1234567890) + }) it('should generate codeVerifier and set codeChallenge to null if clientSecret is not provided', () => { const oauthHandlerWithoutClientSecret = new OAuthHandler( @@ -144,15 +147,16 @@ describe('OAuthHandler', () => { 'appId', 'clientId', 'http://localhost:8184', - null, // No clientSecret - ); + null // No clientSecret + ) // Ensure codeVerifier is generated - expect(oauthHandlerWithoutClientSecret.codeVerifier).to.exist; + // eslint-disable-next-line no-unused-expressions + expect(oauthHandlerWithoutClientSecret.codeVerifier).to.exist // Ensure codeChallenge is null initially - expect(oauthHandlerWithoutClientSecret.codeChallenge).to.equal(null); - }); + expect(oauthHandlerWithoutClientSecret.codeChallenge).to.equal(null) + }) it('should not generate codeVerifier or codeChallenge if clientSecret is provided', () => { const oauthHandlerWithClientSecret = new OAuthHandler( @@ -160,13 +164,13 @@ describe('OAuthHandler', () => { 'appId', 'clientId', 'http://localhost:8184', - 'clientSecret', // clientSecret is provided - ); + 'clientSecret' // clientSecret is provided + ) // codeVerifier and codeChallenge should not be set if clientSecret is provided - expect(oauthHandlerWithClientSecret.codeVerifier).to.equal(undefined); - expect(oauthHandlerWithClientSecret.codeChallenge).to.equal(undefined); - }); + expect(oauthHandlerWithClientSecret.codeVerifier).to.equal(undefined) + expect(oauthHandlerWithClientSecret.codeChallenge).to.equal(undefined) + }) it('should generate codeChallenge after calling generateCodeChallenge when clientSecret is not provided', async () => { const oauthHandlerWithoutClientSecret = new OAuthHandler( @@ -174,47 +178,49 @@ describe('OAuthHandler', () => { 'appId', 'clientId', 'http://localhost:8184', - null, // No clientSecret - ); + null // No clientSecret + ) - const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier; - expect(codeVerifier).to.exist; // Ensure codeVerifier is generated + const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier + // eslint-disable-next-line no-unused-expressions + expect(codeVerifier).to.exist // Ensure codeVerifier is generated - const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier); + const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier) // Ensure the codeChallenge is a URL-safe Base64 string - expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/); // URL-safe Base64 - }); + expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/) // URL-safe Base64 + }) it('should use the Web Crypto API in a browser environment', async () => { // Mock the browser environment global.window = { crypto: { subtle: { - digest: sinon.stub().resolves(new Uint8Array([1, 2, 3, 4])), // Mock hash result - }, - }, - }; + digest: sinon.stub().resolves(new Uint8Array([1, 2, 3, 4])) // Mock hash result + } + } + } const oauthHandlerWithoutClientSecret = new OAuthHandler( axiosInstance, 'appId', 'clientId', 'http://localhost:8184', - null, // No clientSecret - ); + null // No clientSecret + ) - const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier; - expect(codeVerifier).to.exist; // Ensure codeVerifier is generated + const codeVerifier = oauthHandlerWithoutClientSecret.codeVerifier + // eslint-disable-next-line no-unused-expressions + expect(codeVerifier).to.exist // Ensure codeVerifier is generated - const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier); + const codeChallenge = await oauthHandlerWithoutClientSecret.generateCodeChallenge(codeVerifier) // Ensure the codeChallenge is a URL-safe Base64 string - expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/); // URL-safe Base64 + expect(codeChallenge).to.match(/^[A-Za-z0-9-_]+$/) // URL-safe Base64 // Clean up after the test to avoid affecting other tests - delete global.window; - }); + delete global.window + }) it('should generate authorization URL with code_challenge when clientSecret is not provided', async () => { // Mock OAuthHandler without clientSecret @@ -223,22 +229,22 @@ describe('OAuthHandler', () => { 'appId', 'clientId', 'http://localhost:8184', - null, // No clientSecret (PKCE) - ); + null // No clientSecret (PKCE) + ) // Stub the generateCodeChallenge to return a dummy value - const codeChallenge = 'dummyCodeChallenge'; - sandbox.stub(oauthHandler, 'generateCodeChallenge').resolves(codeChallenge); + const codeChallenge = 'dummyCodeChallenge' + sandbox.stub(oauthHandler, 'generateCodeChallenge').resolves(codeChallenge) - const authUrl = await oauthHandler.authorize(); + const authUrl = await oauthHandler.authorize() // Check that code_challenge and code_challenge_method are included in the URL - expect(authUrl).to.include('https://example.com/'); - expect(authUrl).to.include('response_type=code'); - expect(authUrl).to.include('client_id=clientId'); - expect(authUrl).to.include('code_challenge=dummyCodeChallenge'); - expect(authUrl).to.include('code_challenge_method=S256'); - }); + expect(authUrl).to.include('https://example.com/') + expect(authUrl).to.include('response_type=code') + expect(authUrl).to.include('client_id=clientId') + expect(authUrl).to.include('code_challenge=dummyCodeChallenge') + expect(authUrl).to.include('code_challenge_method=S256') + }) // Test cases for getOauthAppAuthorization describe('getOauthAppAuthorization', () => { @@ -248,19 +254,19 @@ describe('OAuthHandler', () => { data: [ { user: { uid: 'currentUserUid' }, - authorization_uid: 'authorizationUid1', - }, - ], - }, - }; + authorization_uid: 'authorizationUid1' + } + ] + } + } - sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + sandbox.stub(axiosInstance, 'get').resolves(mockResponse) - oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid'; - const authorizationUid = await oauthHandler.getOauthAppAuthorization(); + oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid' + const authorizationUid = await oauthHandler.getOauthAppAuthorization() - expect(authorizationUid).to.equal('authorizationUid1'); - }); + expect(authorizationUid).to.equal('authorizationUid1') + }) it('should throw an error when no authorizations found for the current user', async () => { const mockResponse = { @@ -268,58 +274,59 @@ describe('OAuthHandler', () => { data: [ { user: { uid: 'otherUserUid' }, - authorization_uid: 'authorizationUid2', - }, - ], - }, - }; + authorization_uid: 'authorizationUid2' + } + ] + } + } - sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + sandbox.stub(axiosInstance, 'get').resolves(mockResponse) - oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid'; + oauthHandler.axiosInstance.oauth.userUID = 'currentUserUid' try { - await oauthHandler.getOauthAppAuthorization(); - throw new Error('Expected error not thrown'); + await oauthHandler.getOauthAppAuthorization() + throw new Error('Expected error not thrown') } catch (error) { - expect(error.message).to.equal('No authorizations found for current user!'); + expect(error.message).to.equal('No authorizations found for current user!') } - }); + }) it('should throw an error when no authorizations found for the app', async () => { - const mockResponse = { data: { data: [] } }; + const mockResponse = { data: { data: [] } } - sandbox.stub(axiosInstance, 'get').resolves(mockResponse); + sandbox.stub(axiosInstance, 'get').resolves(mockResponse) try { - await oauthHandler.getOauthAppAuthorization(); - throw new Error('Expected error not thrown'); + await oauthHandler.getOauthAppAuthorization() + throw new Error('Expected error not thrown') } catch (error) { - expect(error.message).to.equal('No authorizations found for the app!'); + expect(error.message).to.equal('No authorizations found for the app!') } - }); - }); + }) + }) describe('revokeOauthAppAuthorization', () => { it('should make a DELETE request to revoke authorization when valid authorizationId is provided', async () => { - const authorizationId = 'authorizationUid1'; - const mockResponse = { data: { success: true } }; + const authorizationId = 'authorizationUid1' + const mockResponse = { data: { success: true } } - sandbox.stub(axiosInstance, 'delete').resolves(mockResponse); + sandbox.stub(axiosInstance, 'delete').resolves(mockResponse) + const result = await oauthHandler.revokeOauthAppAuthorization(authorizationId) - const result = await oauthHandler.revokeOauthAppAuthorization(authorizationId); - - expect(result.success).to.be.true; - expect(axiosInstance.delete.calledOnce).to.be.true; - }); + // eslint-disable-next-line no-unused-expressions + expect(result.success).to.be.true + // eslint-disable-next-line no-unused-expressions + expect(axiosInstance.delete.calledOnce).to.be.true + }) it('should not make a DELETE request when authorizationId is invalid or empty', async () => { - const invalidAuthorizationId = ''; - const deleteStub = sandbox.stub(axiosInstance, 'delete'); - - await oauthHandler.revokeOauthAppAuthorization(invalidAuthorizationId); - - expect(deleteStub.called).to.be.false; - }); - }); -}); + const invalidAuthorizationId = '' + const deleteStub = sandbox.stub(axiosInstance, 'delete') + + await oauthHandler.revokeOauthAppAuthorization(invalidAuthorizationId) + // eslint-disable-next-line no-unused-expressions + expect(deleteStub.called).to.be.false + }) + }) +}) From c141f42ca618721da953d95a5de6f6919ebc43be Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Mar 2025 11:21:00 +0530 Subject: [PATCH 08/12] fix: oauth unit test cases --- lib/core/contentstackHTTPClient.js | 6 +++--- test/unit/ContentstackHTTPClient-test.js | 2 +- test/unit/oauthHandler-test.js | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/core/contentstackHTTPClient.js b/lib/core/contentstackHTTPClient.js index 02f590d5..e5d312c7 100644 --- a/lib/core/contentstackHTTPClient.js +++ b/lib/core/contentstackHTTPClient.js @@ -68,11 +68,11 @@ export default function contentstackHttpClient (options) { let uiHostName = hostname let developerHubBaseUrl = hostname - if (hostname.endsWith('io')) { - uiHostName = hostname.replace('io', 'com') + if (uiHostName?.endsWith('io')) { + uiHostName = uiHostName.replace('io', 'com') } - if (hostname.startsWith('api')) { + if (uiHostName?.startsWith('api')) { uiHostName = uiHostName.replace('api', 'app') } const uiBaseUrl = config.endpoint || `${protocol}://${uiHostName}` diff --git a/test/unit/ContentstackHTTPClient-test.js b/test/unit/ContentstackHTTPClient-test.js index 1b78a290..4015e94c 100644 --- a/test/unit/ContentstackHTTPClient-test.js +++ b/test/unit/ContentstackHTTPClient-test.js @@ -112,7 +112,7 @@ describe('Contentstack HTTP Client', () => { it('Contentstack retryDelayOption base test', done => { const client = contentstackHTTPClient({ - retryDelayOptions: { base: 200 } + retryDelayOptions: { base: 200 }, }) expect(client.defaults.retryDelayOptions).to.not.equal(undefined) expect(client.defaults.retryDelayOptions.base).to.be.equal(200) diff --git a/test/unit/oauthHandler-test.js b/test/unit/oauthHandler-test.js index dae925e1..f1e1e15b 100644 --- a/test/unit/oauthHandler-test.js +++ b/test/unit/oauthHandler-test.js @@ -59,7 +59,6 @@ describe('OAuthHandler', () => { const result = await oauthHandler.exchangeCodeForToken('authorization_code') expect(result).to.deep.equal(tokenData) - expect(result).to.include('data') }) it('should refresh access token', async () => { From a86ec2d3ed205551bcc253647245411ccc79753f Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Mar 2025 11:34:28 +0530 Subject: [PATCH 09/12] fix:linting issue in test/unit/ContentstackHTTPClient-test.js --- test/unit/ContentstackHTTPClient-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/ContentstackHTTPClient-test.js b/test/unit/ContentstackHTTPClient-test.js index 4015e94c..1b78a290 100644 --- a/test/unit/ContentstackHTTPClient-test.js +++ b/test/unit/ContentstackHTTPClient-test.js @@ -112,7 +112,7 @@ describe('Contentstack HTTP Client', () => { it('Contentstack retryDelayOption base test', done => { const client = contentstackHTTPClient({ - retryDelayOptions: { base: 200 }, + retryDelayOptions: { base: 200 } }) expect(client.defaults.retryDelayOptions).to.not.equal(undefined) expect(client.defaults.retryDelayOptions.base).to.be.equal(200) From 97876b4c61ab5853ad716045bf9f8120375af57e Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Mar 2025 17:35:28 +0530 Subject: [PATCH 10/12] feat: oauth integration test cases --- test/sanity-check/api/oauth-test.js | 144 ++++++++++++++++++++++++++++ test/sanity-check/sanity.js | 1 + 2 files changed, 145 insertions(+) create mode 100644 test/sanity-check/api/oauth-test.js diff --git a/test/sanity-check/api/oauth-test.js b/test/sanity-check/api/oauth-test.js new file mode 100644 index 00000000..c06dc556 --- /dev/null +++ b/test/sanity-check/api/oauth-test.js @@ -0,0 +1,144 @@ +import { expect } from 'chai' +import { describe, it } from 'mocha' +import { contentstackClient } from '../../sanity-check/utility/ContentstackClient' +import axios from 'axios' +import dotenv from 'dotenv' + +dotenv.config() +let accessToken = '' +let loggedinUserID = '' +let authUrl = '' +let codeChallenge = '' +let codeChallengeMethod = '' +let authCode +let authtoken = '' +let redirectUrl = '' +let refreshToken = '' +const client = contentstackClient() +const oauthClient = client.oauth({ + clientId: process.env.CLIENT_ID, + appId: process.env.APP_ID, + redirectUri: process.env.REDIRECT_URI +}) + +describe('OAuth Authentication API Test', () => { + it('should login with credentials', done => { + client.login({ email: process.env.EMAIL, password: process.env.PASSWORD }, { include_orgs: true, include_orgs_roles: true, include_stack_roles: true, include_user_settings: true }).then((response) => { + expect(response.notice).to.be.equal('Login Successful.', 'Login success messsage does not match.') + done() + }) + .catch(done) + }) + + it('should get Current user info test', done => { + client.getUser().then((user) => { + authtoken = user.authtoken + done() + }) + .catch(done) + }) + + it('should fail when trying to login with invalid app credentials', () => { + try { + client.oauth({ + clientId: 'clientId', + appId: 'appId', + redirectUri: 'redirectUri' + }) + } catch (error) { + const jsonMessage = JSON.parse(error.message) + expect(jsonMessage.status).to.be.equal(401, 'Status code does not match for invalid credentials') + expect(jsonMessage.errorMessage).to.not.equal(null, 'Error message not proper') + expect(jsonMessage.errorCode).to.be.equal(104, 'Error code does not match') + } + }) + + it('should generate OAuth authorization URL', async () => { + authUrl = await oauthClient.authorize() + const url = new URL(authUrl) + + codeChallenge = url.searchParams.get('code_challenge') + codeChallengeMethod = url.searchParams.get('code_challenge_method') + + // Ensure they are not empty strings + expect(codeChallenge).to.not.equal('') + expect(codeChallengeMethod).to.not.equal('') + expect(authUrl).to.include(process.env.CLIENT_ID, 'Client ID mismatch') + }) + + it('should simulate calling the authorization URL and receive authorization code', async () => { + try { + const authorizationEndpoint = oauthClient.axiosInstance.defaults.developerHubBaseUrl + axios.defaults.headers.common.authtoken = authtoken + axios.defaults.headers.common.organization_uid = process.env.ORGANIZATION + const response = await axios + .post(`${authorizationEndpoint}/manifests/${process.env.APP_ID}/authorize`, { + client_id: process.env.CLIENT_ID, + redirect_uri: process.env.REDIRECT_URI, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + response_type: 'code' + }) + const data = response.data + redirectUrl = data.data.redirect_url + const url = new URL(redirectUrl) + authCode = url.searchParams.get('code') + oauthClient.axiosInstance.oauth.appId = process.env.APP_ID + oauthClient.axiosInstance.oauth.clientId = process.env.CLIENT_ID + oauthClient.axiosInstance.oauth.redirectUri = process.env.REDIRECT_URI + // Ensure they are not empty strings + expect(redirectUrl).to.not.equal('') + expect(url).to.not.equal('') + } catch (error) { + console.log(error) + } + }) + + it('should exchange authorization code for access token', async () => { + const response = await oauthClient.exchangeCodeForToken(authCode) + accessToken = response.access_token + loggedinUserID = response.user_uid + refreshToken = response.refresh_token + + expect(response.organization_uid).to.be.equal(process.env.ORGANIZATION, 'Organization mismatch') + // eslint-disable-next-line no-unused-expressions + expect(response.access_token).to.not.be.null + // eslint-disable-next-line no-unused-expressions + expect(response.refresh_token).to.not.be.null + }) + + it('should get the logged-in user info using the access token', async () => { + const user = await client.getUser({ + authorization: `Bearer ${accessToken}` + }) + expect(user.uid).to.be.equal(loggedinUserID) + expect(user.email).to.be.equal(process.env.EMAIL, 'Email mismatch') + }) + + it('should refresh the access token using refresh token', async () => { + const response = await oauthClient.refreshAccessToken(refreshToken) + + accessToken = response.access_token + refreshToken = response.refresh_token + // eslint-disable-next-line no-unused-expressions + expect(response.access_token).to.not.be.null + // eslint-disable-next-line no-unused-expressions + expect(response.refresh_token).to.not.be.null + }) + + it('should logout successfully after OAuth authentication', async () => { + const response = await oauthClient.logout() + expect(response).to.be.equal('Logged out successfully') + }) + + it('should fail to make an API request with an expired token', async () => { + try { + await client.getUser({ + authorization: `Bearer ${accessToken}` + }) + } catch (error) { + expect(error.status).to.be.equal(401, 'API request should fail with status 401') + expect(error.errorMessage).to.be.equal('The provided access token is invalid or expired or revoked', 'Error message mismatch') + } + }) +}) diff --git a/test/sanity-check/sanity.js b/test/sanity-check/sanity.js index bf309516..8ee08d31 100644 --- a/test/sanity-check/sanity.js +++ b/test/sanity-check/sanity.js @@ -29,3 +29,4 @@ require('./api/contentType-delete-test') require('./api/delete-test') require('./api/team-test') require('./api/auditlog-test') +require('./api/oauth-test') From b310d86b7dad4766566622ffc793a272757c1ddb Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Mon, 17 Mar 2025 19:41:07 +0530 Subject: [PATCH 11/12] fix: refresh &updated token url --- lib/core/oauthHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/oauthHandler.js b/lib/core/oauthHandler.js index 3a42edb6..aa46c8f5 100644 --- a/lib/core/oauthHandler.js +++ b/lib/core/oauthHandler.js @@ -133,7 +133,7 @@ export default class OAuthHandler { this.axiosInstance.defaults.headers = this._getHeaders() try { - const response = await this.axiosInstance.post(`${this.OAuthBaseURL}/apps-api/apps/token`, body) + const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/token`, body) this._saveTokens(response.data) return response.data @@ -180,7 +180,7 @@ export default class OAuthHandler { this.axiosInstance.defaults.headers = this._getHeaders() try { - const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/apps/token`, body) + const response = await this.axiosInstance.post(`${this.developerHubBaseUrl}/token`, body) const data = response.data this.axiosInstance.oauth.accessToken = data.access_token From ee516f0e8228cc101078650f6fd6615d745cbe6b Mon Sep 17 00:00:00 2001 From: Aman Kumar Date: Wed, 26 Mar 2025 10:52:44 +0530 Subject: [PATCH 12/12] updated axios version & change log --- CHANGELOG.md | 7 +++++++ package-lock.json | 10 ++++++---- package.json | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08500cf7..c203aacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [v1.20.0](https://github.com/contentstack/contentstack-management-javascript/tree/v1.20.0) (2025-04-01) + - Feature + - Added OAuth support + - Added the Unit Test cases and added sanity test case for OAuth + - Handle retry the requests that were pending due to token expiration + - Updated Axios Version + ## [v1.19.5](https://github.com/contentstack/contentstack-management-javascript/tree/v1.19.5) (2025-03-17) - Fix - Added AuditLog in the stack class diff --git a/package-lock.json b/package-lock.json index b438595b..95c52f07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@contentstack/management", - "version": "1.19.5", + "version": "1.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/management", - "version": "1.19.5", + "version": "1.20.0", "license": "MIT", "dependencies": { "assert": "^2.1.0", - "axios": "^1.8.2", + "axios": "^1.8.4", "buffer": "^6.0.3", "form-data": "^4.0.2", "lodash": "^4.17.21", @@ -3695,7 +3695,9 @@ } }, "node_modules/axios": { - "version": "1.8.2", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", diff --git a/package.json b/package.json index a7c684dc..e7c3769a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/management", - "version": "1.19.5", + "version": "1.20.0", "description": "The Content Management API is used to manage the content of your Contentstack account", "main": "./dist/node/contentstack-management.js", "browser": "./dist/web/contentstack-management.js", @@ -53,7 +53,7 @@ "license": "MIT", "dependencies": { "assert": "^2.1.0", - "axios": "^1.8.2", + "axios": "^1.8.4", "buffer": "^6.0.3", "form-data": "^4.0.2", "lodash": "^4.17.21",