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..6884ef83 --- /dev/null +++ b/lib/core/oauthHandler.js @@ -0,0 +1,325 @@ +/** + * @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 { + // 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(); + + // 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 + * @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..f4bc9dcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,13 @@ "version": "1.19.2", "license": "MIT", "dependencies": { + "assert": "^2.1.0", "axios": "^1.7.9", + "buffer": "^6.0.3", "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 +4053,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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 +4095,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 +4644,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", @@ -4849,6 +4882,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", @@ -4922,7 +4978,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", @@ -5555,7 +5610,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 +5627,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", @@ -6941,7 +6994,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 +7456,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 +7496,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" @@ -7553,6 +7603,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 +7774,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 +7825,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 +7956,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 +8072,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 +8112,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 +8234,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 +8329,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" @@ -11511,7 +11589,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 +11605,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 +11614,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", @@ -12112,7 +12187,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" @@ -12365,6 +12439,19 @@ "dev": true, "license": "MIT" }, + "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/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -12878,7 +12965,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 +13002,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 +13145,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", @@ -13406,6 +13490,23 @@ "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/string_decoder": { + "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.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -14401,6 +14502,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 +14883,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..a5db1347 100644 --- a/package.json +++ b/package.json @@ -52,10 +52,13 @@ "author": "Contentstack", "license": "MIT", "dependencies": { + "assert": "^2.1.0", "axios": "^1.7.9", + "buffer": "^6.0.3", "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..965876a5 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: false, + 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..aa0cbb11 100644 --- a/webpack/webpack.node.js +++ b/webpack/webpack.node.js @@ -10,6 +10,16 @@ module.exports = function (options) { filename: 'contentstack-management.js' }, target: 'node', + resolve: { + fallback: { + os: require.resolve('os-browserify/browser'), + fs: false, + crypto: false, + 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..e2aaa4c3 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: false, + 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..e20f7016 100644 --- a/webpack/webpack.web.js +++ b/webpack/webpack.web.js @@ -16,8 +16,12 @@ module.exports = function (options) { resolve: { fallback: { os: require.resolve('os-browserify/browser'), - fs: false - } + fs: false, + crypto: false, + stream: require.resolve('stream-browserify'), + assert: require.resolve('assert'), + buffer: require.resolve('buffer') + }, }, module: { rules: [{