Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/core/src/destination-kit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ interface AuthSettings<Settings> {
interface RefreshAuthSettings<Settings> {
settings: Settings
auth: OAuth2ClientCredentials
features?: Features
}

interface Authentication<Settings> {
Expand Down Expand Up @@ -564,7 +565,8 @@ export class Destination<Settings = JSONObject, AudienceSettings = JSONObject> {
async refreshAccessToken(
settings: Settings,
oauthData: OAuth2ClientCredentials,
synchronizeRefreshAccessToken?: () => Promise<void>
synchronizeRefreshAccessToken?: () => Promise<void>,
features?: Features
): Promise<RefreshAccessTokenResult | undefined> {
if (!(this.authentication?.scheme === 'oauth2' || this.authentication?.scheme === 'oauth-managed')) {
throw new IntegrationError(
Expand All @@ -590,7 +592,7 @@ export class Destination<Settings = JSONObject, AudienceSettings = JSONObject> {
// Invoke synchronizeRefreshAccessToken handler if synchronizeRefreshAccessToken option is passed.
// This will ensure that there is only one active refresh happening at a time.
await synchronizeRefreshAccessToken?.()
return this.authentication.refreshAccessToken(requestClient, { settings, auth: oauthData })
return this.authentication.refreshAccessToken(requestClient, { settings, auth: oauthData, features })
}

private partnerAction(
Expand Down Expand Up @@ -1062,7 +1064,8 @@ export class Destination<Settings = JSONObject, AudienceSettings = JSONObject> {
const newTokens = await this.refreshAccessToken(
destinationSettings,
oauthSettings,
options?.synchronizeRefreshAccessToken
options?.synchronizeRefreshAccessToken,
options?.features
)

if (!newTokens) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { HUBSPOT_BASE_URL } from '../properties'

const testDestination = createTestIntegration(Definition)

const oauthData = {
refreshToken: 'refresh-token',
accessToken: 'access-token',
clientId: 'client-id',
clientSecret: 'client-secret'
}

describe('HubSpot Cloud Mode (Actions)', () => {
describe('testAuthentication', () => {
it('should validate authentication inputs', async () => {
Expand All @@ -23,4 +30,37 @@ describe('HubSpot Cloud Mode (Actions)', () => {
)
})
})

describe('refreshAccessToken', () => {
it('should use v1 endpoint when feature flag is not set', async () => {
nock(HUBSPOT_BASE_URL)
.post('/oauth/v1/token')
.reply(200, { access_token: 'new-access-token' })

const result = await testDestination.refreshAccessToken({}, oauthData)
expect(result).toEqual({ accessToken: 'new-access-token' })
})

it('should use v1 endpoint when feature flag is false', async () => {
nock(HUBSPOT_BASE_URL)
.post('/oauth/v1/token')
.reply(200, { access_token: 'new-access-token' })

const result = await testDestination.refreshAccessToken({}, oauthData, undefined, {
'actions-hubspot-oauth-v2': false
})
expect(result).toEqual({ accessToken: 'new-access-token' })
})

it('should use 2026-03 endpoint when feature flag is enabled', async () => {
nock(HUBSPOT_BASE_URL)
.post('/oauth/2026-03/token')
.reply(200, { access_token: 'new-access-token-v2' })

const result = await testDestination.refreshAccessToken({}, oauthData, undefined, {
'actions-hubspot-oauth-v2': true
})
expect(result).toEqual({ accessToken: 'new-access-token-v2' })
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import upsertCustomObjectRecord from './upsertCustomObjectRecord'
import upsertObject from './upsertObject'
import customEvent from './customEvent'
import { HUBSPOT_BASE_URL } from './properties'
import { HUBSPOT_CRM_API_VERSION, HUBSPOT_OAUTH_API_VERSION } from './versioning-info'
import { HUBSPOT_CRM_API_VERSION, HUBSPOT_OAUTH_API_VERSION, HUBSPOT_OAUTH_API_VERSION_NEXT_FLAGON } from './versioning-info'
interface RefreshTokenResponse {
access_token: string
}
Expand All @@ -31,9 +31,9 @@ const destination: DestinationDefinition<Settings> = {
// HubSpot doesn't have a test authentication endpoint, so we using a lightweight CRM API to validate access token
return request(`${HUBSPOT_BASE_URL}/crm/${HUBSPOT_CRM_API_VERSION}/objects/contacts?limit=1`)
},
refreshAccessToken: async (request, { auth }) => {
// Return a request that refreshes the access_token if the API supports it
const res = await request<RefreshTokenResponse>(`${HUBSPOT_BASE_URL}/oauth/${HUBSPOT_OAUTH_API_VERSION}/token`, {
refreshAccessToken: async (request, { auth, features }) => {
const oauthVersion = features?.['actions-hubspot-oauth-v2'] ? HUBSPOT_OAUTH_API_VERSION_NEXT_FLAGON : HUBSPOT_OAUTH_API_VERSION
const res = await request<RefreshTokenResponse>(`${HUBSPOT_BASE_URL}/oauth/${oauthVersion}/token`, {
method: 'POST',
body: new URLSearchParams({
refresh_token: auth.refreshToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ export const HUBSPOT_CRM_API_VERSION = 'v3'
export const HUBSPOT_CRM_ASSOCIATIONS_API_VERSION = 'v4'

/** HUBSPOT_OAUTH_API_VERSION
* HubSpot OAuth API version.
* HubSpot OAuth API version (legacy).
* API reference: https://developers.hubspot.com/docs/api-reference/auth-oauth-v1/tokens/post-oauth-v1-token
*/
export const HUBSPOT_OAUTH_API_VERSION = 'v1'

/** HUBSPOT_OAUTH_API_VERSION_NEXT_FLAGON
* HubSpot OAuth API version (2026-03).
* API reference: https://developers.hubspot.com/docs/api-reference/latest/authentication/manage-oauth-tokens
*/
export const HUBSPOT_OAUTH_API_VERSION_NEXT_FLAGON = '2026-03'
Loading