Files
botpress--botpress/integrations/docusign/src/docusign-api/auth-utils.ts
T
2026-07-13 13:34:48 +08:00

152 lines
4.5 KiB
TypeScript

import { RuntimeError } from '@botpress/sdk'
import { CommonHandlerProps } from '../types'
import { DocusignAuthClient } from './auth'
import { GetAccessTokenResp, UserAccount } from './schemas'
import * as bp from '.botpress'
export const MS_PER_MINUTE = 60000
export const MS_PER_HOUR = MS_PER_MINUTE * 60
export const applyOAuthState = async ({ client, ctx }: CommonHandlerProps, tokenResp: GetAccessTokenResp) => {
const { accessToken, refreshToken, expiresAt, tokenType } = tokenResp
const { state } = await client.setState({
type: 'integration',
name: 'configuration',
id: ctx.integrationId,
payload: {
oauth: {
accessToken,
tokenType,
refreshToken,
expiresAt,
},
},
})
if (!state.payload.oauth) {
throw new Error('Failed to store OAuth state')
}
return state.payload.oauth
}
const OAUTH_TIMEOUT_BUFFER = MS_PER_MINUTE * 5
export const getOAuthState = async (props: CommonHandlerProps, authClient?: DocusignAuthClient) => {
const { state } = await props.client.getOrSetState({
type: 'integration',
name: 'configuration',
id: props.ctx.integrationId,
payload: {
oauth: null,
},
})
let oauthState = state.payload.oauth
if (!oauthState) {
throw new RuntimeError('User authentication has not been completed')
}
const { expiresAt, refreshToken } = oauthState
if (expiresAt - OAUTH_TIMEOUT_BUFFER <= Date.now()) {
authClient ??= DocusignAuthClient.create(props)
const tokenResp = await authClient.getAccessTokenWithRefreshToken(refreshToken)
if (!tokenResp.success) throw tokenResp.error
oauthState = await applyOAuthState(props, tokenResp.data)
}
return oauthState
}
const _findAccount = (accountsList: UserAccount[], explicitAccountId: string | undefined): UserAccount => {
let account: UserAccount | null = null
if (explicitAccountId) {
account = accountsList.find(({ account_id }) => account_id === explicitAccountId) ?? null
if (!account) {
throw new RuntimeError('An account with the specified API Account ID does not exist or is not owned by this user')
}
} else {
account = accountsList.find(({ is_default }: UserAccount) => is_default) ?? accountsList[0]!
}
return account
}
const ACCOUNT_REFRESH_AFTER = MS_PER_HOUR * 24
export const refreshAccountState = async (props: CommonHandlerProps) => {
const authClient = DocusignAuthClient.create(props)
const { accessToken, tokenType } = await getOAuthState(props, authClient)
const userInfoResp = await authClient.getUserInfo(accessToken, tokenType)
if (!userInfoResp.success) throw userInfoResp.error
const { client, ctx } = props
const { accounts } = userInfoResp.data
const explicitAccountId = ctx.configuration.accountId
const account = _findAccount(accounts, explicitAccountId)
const refreshAt = !explicitAccountId?.trim() ? Date.now() + ACCOUNT_REFRESH_AFTER : null
const { state } = await client.setState({
type: 'integration',
name: 'account',
id: ctx.integrationId,
payload: {
account: {
id: account.account_id,
baseUri: account.base_uri,
refreshAt,
},
},
})
if (!state.payload.account) {
throw new RuntimeError('Failed to store account state')
}
return state.payload.account
}
export const getAccountState = async (props: CommonHandlerProps) => {
const { state } = await props.client.getOrSetState({
type: 'integration',
name: 'account',
id: props.ctx.integrationId,
payload: {
account: null,
},
})
let hasAccountChanged = false
let accountState = state.payload.account
if (!accountState || (accountState.refreshAt && accountState.refreshAt <= Date.now())) {
const prevAccountState = accountState
accountState = await refreshAccountState(props)
if (accountState.id !== prevAccountState?.id) {
hasAccountChanged = true
}
}
return { account: accountState, hasChanged: hasAccountChanged }
}
export const exchangeAuthCodeForRefreshToken = async (props: bp.HandlerProps, oAuthCode: string): Promise<void> => {
const authClient = DocusignAuthClient.create(props)
const tokenResp = await authClient.getAccessTokenWithCode(oAuthCode)
if (!tokenResp.success) throw tokenResp.error
const userInfoResp = await authClient.getUserInfo(tokenResp.data.accessToken, tokenResp.data.tokenType)
if (!userInfoResp.success) throw userInfoResp.error
await applyOAuthState(props, tokenResp.data)
await props.client.configureIntegration({
identifier: userInfoResp.data.sub,
})
}