chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:34:48 +08:00
commit 77bb5bf71f
3762 changed files with 353249 additions and 0 deletions
@@ -0,0 +1,151 @@
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,
})
}
@@ -0,0 +1,141 @@
import { handleErrorsDecorator as handleErrors } from '@botpress/common'
import { RuntimeError } from '@botpress/sdk'
import axios, { type AxiosInstance } from 'axios'
import type { CommonHandlerProps, Result } from '../types'
import {
type GetAccessTokenResp,
docusignOAuthAccessTokenRespSchema,
type GetUserInfoResp,
getUserInfoRespSchema,
} from './schemas'
import { GetAccessTokenParams } from './types'
import * as bp from '.botpress'
type OAuthParameters = {
oauthBaseUrl: string
clientId: string
clientSecret: string
}
export class DocusignAuthClient {
private _axiosClient: AxiosInstance
private constructor(params: OAuthParameters) {
const { oauthBaseUrl, clientId, clientSecret } = params
// Opted for axios here since the docusign package only has
// a function for getting an accessToken from the oauth code
// but not for refresh tokens
this._axiosClient = axios.create({
baseURL: oauthBaseUrl,
headers: {
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
'Cache-Control': 'no-store',
Pragma: 'no-cache',
},
})
}
private async _getAccessToken(params: GetAccessTokenParams): Promise<Result<GetAccessTokenResp>> {
// The Docusign API Postman collection example uses FormData for this endpoint.
const formData = new FormData()
Object.entries(params).forEach(([key, value]) => formData.append(key, value))
// Docusign doesn't return a timestamp when a token is issued. So a timestamp
// is generated prior to the request being made so the expiry time is accurate.
const tokenRequestedAt = Date.now()
const resp = await this._axiosClient.post('/oauth/token', formData)
if (resp.status < 200 || resp.status >= 300) {
return {
success: false,
error: new RuntimeError(
`Failed to retrieve access token w/${params.grant_type} | Invalid Status '${resp.status}'`
),
}
}
const result = docusignOAuthAccessTokenRespSchema.safeParse(resp.data)
if (!result.success) {
return {
success: false,
error: new RuntimeError(`Failed to retrieve access token w/${params.grant_type} | Schema Parse Failure`),
}
}
return {
success: true,
data: {
accessToken: result.data.access_token,
tokenType: result.data.token_type,
expiresAt: tokenRequestedAt + result.data.expires_in * 1000,
refreshToken: result.data.refresh_token,
},
}
}
@handleErrors('Failed to obtain Docusign OAuth access token from authorization code')
public async getAccessTokenWithCode(code: string): Promise<Result<GetAccessTokenResp>> {
return this._getAccessToken({
grant_type: 'authorization_code',
code,
})
}
@handleErrors('Failed to refresh Docusign OAuth access token')
public async getAccessTokenWithRefreshToken(refreshToken: string): Promise<Result<GetAccessTokenResp>> {
return this._getAccessToken({
grant_type: 'refresh_token',
refresh_token: refreshToken,
})
}
@handleErrors('Failed to retrieve Docusign user info')
public async getUserInfo(accessToken: string, tokenType: string): Promise<Result<GetUserInfoResp>> {
const resp = await this._axiosClient.get('/oauth/userinfo', {
headers: {
Authorization: `${tokenType} ${accessToken}`,
},
})
if (resp.status < 200 || resp.status >= 300) {
return {
success: false,
error: new RuntimeError(`Failed to retrieve user info | Invalid Status '${resp.status}'`),
}
}
const result = getUserInfoRespSchema.safeParse(resp.data)
if (!result.success) {
return {
success: false,
error: new RuntimeError('Failed to retrieve user info | Schema Parse Failure'),
}
}
return { success: true, data: result.data }
}
public static create(props: CommonHandlerProps): DocusignAuthClient {
const { ctx } = props
switch (ctx.configurationType) {
case 'sandbox':
return new DocusignAuthClient({
oauthBaseUrl: bp.secrets.SANDBOX_OAUTH_BASE_URL,
clientId: bp.secrets.SANDBOX_CLIENT_ID,
clientSecret: bp.secrets.SANDBOX_CLIENT_SECRET,
})
case null:
return new DocusignAuthClient({
oauthBaseUrl: bp.secrets.OAUTH_BASE_URL,
clientId: bp.secrets.CLIENT_ID,
clientSecret: bp.secrets.CLIENT_SECRET,
})
default:
ctx satisfies never
}
throw new RuntimeError(`Unsupported configuration type: ${props.ctx.configurationType}`)
}
}
@@ -0,0 +1,33 @@
import { TemplateRecipient, SendEnvelopeInput } from 'definitions/actions'
import docusign from 'docusign-esign'
import { CONVERSATION_ID_FIELD_KEY } from '../config'
const _createTemplateRecipient = (recipientInfo: TemplateRecipient): docusign.TemplateRole => {
const { email, name, role, accessCode } = recipientInfo
return {
email,
name,
roleName: role,
accessCode,
}
}
export const sendEnvelopeInputToEnvelopeDefinition = (input: SendEnvelopeInput): docusign.EnvelopeDefinition => {
const { emailSubject, templateId, recipients, conversationId } = input
return {
emailSubject,
templateId,
templateRoles: recipients.map(_createTemplateRecipient),
status: 'sent',
customFields: {
textCustomFields: [
{
name: CONVERSATION_ID_FIELD_KEY,
value: conversationId,
show: 'false',
},
],
},
}
}
@@ -0,0 +1,111 @@
import { RuntimeError } from '@botpress/sdk'
import axios, { AxiosInstance } from 'axios'
import docusign from 'docusign-esign'
import type { CommonHandlerProps } from '../types'
import { getAccountState, getOAuthState } from './auth-utils'
import { constructWebhookBody, refreshWebhooks } from './utils'
type DocusignClientParams = {
accountId: string
baseUri: string
accessToken: string
tokenType: string
}
export class DocusignClient {
private _axiosClient: AxiosInstance
private _accountId: string
private constructor(params: DocusignClientParams) {
const { baseUri, tokenType, accessToken, accountId } = params
this._accountId = accountId
this._axiosClient = axios.create({
baseURL: `${baseUri}/restapi/v2.1`,
headers: {
Authorization: `${tokenType} ${accessToken}`,
},
})
}
public async sendEnvelope(envelope: docusign.EnvelopeDefinition) {
try {
const resp = await this._axiosClient.post<docusign.EnvelopeSummary>(
`/accounts/${this._accountId}/envelopes`,
envelope
)
const { data } = resp
if (!data.envelopeId) {
const message = data.errorDetails ? data.errorDetails.message : 'Did not receive EnvelopeID from Docusign'
throw new Error(message)
}
return {
envelopeId: data.envelopeId,
}
} catch (thrown: unknown) {
const err = thrown instanceof Error ? thrown : new Error(String(thrown))
throw new RuntimeError('Failed to send envelope', err)
}
}
public async listWebhooks(): Promise<docusign.ConnectConfigResults['configurations']> {
const resp = await this._axiosClient.get<docusign.ConnectConfigResults>(`/accounts/${this._accountId}/connect`)
return resp.data.configurations
}
public async createWebhook(webhookUrl: string, botId: string): Promise<string> {
try {
const body = constructWebhookBody(webhookUrl, botId)
const resp = await this._axiosClient.post<docusign.ConnectCustomConfiguration>(
`/accounts/${this._accountId}/connect`,
body
)
const { connectId } = resp.data
if (!connectId) {
throw new RuntimeError('Failed to create webhook due to unexpected api response')
}
return connectId
} catch (thrown: unknown) {
if (thrown instanceof RuntimeError) {
throw thrown
}
const error =
thrown instanceof Error ? new RuntimeError(thrown.message, thrown) : new RuntimeError(String(thrown))
throw error
}
}
public async removeWebhook(connectId: string): Promise<boolean> {
try {
await this._axiosClient.delete(`/accounts/${this._accountId}/connect/${connectId}`)
return true
} catch {
return false
}
}
/** Creates a docusign api client from the oauth parameters */
public static async create(props: CommonHandlerProps): Promise<DocusignClient> {
const [oauthState, accountState] = await Promise.all([getOAuthState(props), getAccountState(props)])
const { account, hasChanged: hasAccountChanged } = accountState
const apiClient = new DocusignClient({
accountId: account.id,
baseUri: account.baseUri,
accessToken: oauthState.accessToken,
tokenType: oauthState.tokenType,
})
// This side-effect doesn't really belong here, but I can't put
// it anywhere else without it leading to fragile behaviour.
if (hasAccountChanged) {
await refreshWebhooks(props, process.env.BP_WEBHOOK_URL!, apiClient)
}
return apiClient
}
}
@@ -0,0 +1,36 @@
import { z } from '@botpress/sdk'
export const docusignOAuthAccessTokenRespSchema = z.object({
access_token: z.string(),
token_type: z.string().describe('The authentication header type (e.g. "Bearer")'),
expires_in: z.number().describe('Seconds until the token expires'),
refresh_token: z.string(),
scope: z.string(),
})
export type GetAccessTokenResp = {
accessToken: string
/** The authentication header type for the access token (e.g. "Bearer") */
tokenType: string
/** The expiry time of the access token represented as a Unix timestamp (milliseconds) */
expiresAt: number
refreshToken: string
}
const _userAccountSchema = z
.object({
account_id: z.string(),
account_name: z.string(),
is_default: z.boolean(),
base_uri: z.string().url(),
})
.strip()
export type UserAccount = z.infer<typeof _userAccountSchema>
export const getUserInfoRespSchema = z
.object({
sub: z.string().min(1),
accounts: z.array(_userAccountSchema).min(1),
})
.strip()
export type GetUserInfoResp = z.infer<typeof getUserInfoRespSchema>
@@ -0,0 +1,9 @@
export type GetAccessTokenParams =
| {
grant_type: 'authorization_code'
code: string
}
| {
grant_type: 'refresh_token'
refresh_token: string
}
@@ -0,0 +1,53 @@
import { CommonHandlerProps } from '../types'
import { DocusignClient } from '.'
export const constructWebhookBody = (webhookUrl: string, botId: string, additionalProps: object = {}) => {
return {
configurationType: 'custom',
urlToPublishTo: webhookUrl,
name: `Botpress Integration | Bot ID: ${botId}`,
allowEnvelopePublish: 'true',
enableLog: 'true',
deliveryMode: 'SIM',
requiresAcknowledgement: 'true',
signMessageWithX509Certificate: 'true',
includeTimeZoneInformation: 'true',
includeHMAC: 'true',
includeEnvelopeVoidReason: 'false',
includeSenderAccountasCustomField: 'true',
integratorManaged: 'true',
envelopeEvents: ['Sent', 'Delivered', 'Completed', 'Declined', 'Voided'],
events: ['envelope-resent', 'envelope-reminder-sent'],
allUsers: 'true',
eventData: {
version: 'restv2.1',
includeData: ['custom_fields'],
},
...additionalProps,
}
}
export const cleanupWebhooks = async (props: CommonHandlerProps, webhookUrl: string, apiClient?: DocusignClient) => {
apiClient ??= await DocusignClient.create(props)
const resp = await apiClient.listWebhooks()
const webhookDeletionPromises = resp?.reduce((webhookPromises, configuration) => {
const { urlToPublishTo, connectId } = configuration
if (!connectId || urlToPublishTo !== webhookUrl) {
return webhookPromises
}
const promise = apiClient.removeWebhook(connectId)
return webhookPromises.concat(promise)
}, [] as Promise<boolean>[])
await Promise.allSettled(webhookDeletionPromises ?? [])
}
export const refreshWebhooks = async (props: CommonHandlerProps, webhookUrl: string, apiClient?: DocusignClient) => {
apiClient ??= await DocusignClient.create(props)
await cleanupWebhooks(props, webhookUrl, apiClient)
await apiClient.createWebhook(webhookUrl, props.ctx.botId)
}