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
+13
View File
@@ -0,0 +1,13 @@
import rootConfig from '../../eslint.config.mjs'
export default [
...rootConfig,
{
languageOptions: {
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname,
},
},
},
]
+37
View File
@@ -0,0 +1,37 @@
Connect your Botpress chatbot with Stripe, a popular online payment platform that facilitates transactions between businesses and their customers. Stripe allows you to manage payments, subscriptions, invoices, and more.
## Setup and Configuration
The Stripe integration supports two authentication modes:
- **OAuth (recommended)** — authorize Botpress on your Stripe account through Stripe's hosted consent screen. No keys to copy or rotate.
- **API Key (manual)** — paste a Stripe Secret Key (`sk_live_…` / `sk_test_…`) or a Restricted Key.
### Prerequisites
- A Botpress cloud account.
- A Stripe account.
### Enable Integration
1. Open your bot in the Botpress dashboard and navigate to the "Integrations" section.
2. Locate the Stripe integration and click on "Configure".
3. Click the configuration link to open the setup wizard.
4. Choose **Connect with OAuth** to be redirected to Stripe's authorization page, or choose **Use a Stripe API Key** to paste a key from the [Stripe Dashboard](https://dashboard.stripe.com/apikeys).
5. Save the configuration.
The integration will create a webhook endpoint on your Stripe account on first use. The webhook signing secret is captured automatically and used to verify all incoming events.
## Usage
Once the integration is enabled, you can start using Stripe features from your Botpress chatbot. The integration offers several actions for interacting with Stripe, such as `createPaymentLink` , `createSubsLink` (For generate Subscription Payment Link), `listPaymentLinks` (IDs and URLs), `listProductPrices` (If price has the "recurring" property, the product is of type subscription.), `findPaymentLink` (By URL, return ID), and `deactivatePaymentLink` (By ID). And actions for Customers, `listCustomers` (Optional filter by e-mail), `searchCustomers` (By e-mail, name or/and phone), `createCustomer` and `createOrRetrieveCustomer` (If the user already exists, his email has already been registered, get it. If there are multiple users with the same email, return an array of them. If it does not exist, it creates it).
## Supported Events
- **Charge Failed**: This event occurs when a charge fails in Stripe.
- **Subscription Deleted**: This event occurs when a subscription is canceled/deleted in Stripe.
- **Subscription Updated**: This event occurs when a subscription is updated in Stripe. For example, when the subscription is canceled, but does not terminate immediately, `cancel_at_period_end` becomes true.
- **Invoice Payment Failed**: This event occurs when an invoice payment fails in Stripe.
- **Payment Intent Failed**: This event occurs when a payment intent fails in Stripe.
These events allow your chatbot to respond to various situations related to charges, payments and subscriptions in Stripe.
+4
View File
@@ -0,0 +1,4 @@
<svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="150" height="150" rx="75" fill="#6772E5"/>
<path d="M68.4886 56.3742C68.4886 52.28 71.9014 50.7029 77.373 50.7029C85.341 50.7029 95.453 53.1435 103.421 57.4403V32.7902C94.7394 29.3216 86.0753 28 77.3906 28C56.1533 28 42 39.0871 42 57.6253C42 86.6104 81.7961 81.9112 81.7961 94.411C81.7961 99.257 77.5933 100.817 71.7545 100.817C63.0905 100.817 51.8917 97.2364 43.1014 92.4638V116.051C52.145 119.953 61.8874 121.976 71.7369 121.998C93.5088 121.998 108.505 112.635 108.505 93.803C108.505 62.5624 68.4886 68.1603 68.4886 56.3742V56.3742Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 671 B

@@ -0,0 +1,286 @@
import { IntegrationDefinition, z } from '@botpress/sdk'
import {
createCustomerInputSchema,
createCustomerOutputSchema,
createOrRetrieveCustomerInputSchema,
createOrRetrieveCustomerOutputSchema,
createPaymentLinkInputSchema,
createPaymentLinkOutputSchema,
createSubsLinkInputSchema,
createSubsLinkOutputSchema,
deactivatePaymentLinkInputSchema,
deactivatePaymentLinkOutputSchema,
findPaymentLinkInputSchema,
findPaymentLinkOutputSchema,
listCustomersInputSchema,
listCustomersOutputSchema,
listPaymentLinksInputSchema,
listPaymentLinksOutputSchema,
listProductPricesInputSchema,
listProductPricesOutputSchema,
retrieveCustomerByIdInputSchema,
retrieveCustomerByIdOutputSchema,
searchCustomersInputSchema,
searchCustomersOutputSchema,
chargeFailedSchema,
invoicePaymentFailedSchema,
paymentIntentFailedSchema,
subscriptionDeletedSchema,
subscriptionUpdatedSchema,
subscriptionCreatedSchema,
subscriptionScheduleCreatedSchema,
subscriptionScheduleUpdatedSchema,
} from './src/misc/custom-schemas'
export default new IntegrationDefinition({
name: 'stripe',
version: '0.6.1',
title: 'Stripe',
readme: 'hub.md',
icon: 'icon.svg',
description:
'Manage payments, subscriptions, and customers seamlessly. Execute workflows on charge failures and subscription updates.',
configuration: {
identifier: {
linkTemplateScript: 'linkTemplate.vrl',
},
schema: z.object({
apiVersion: z
.string()
.optional()
.default('2023-10-16')
.describe('API Version (optional) (default: 2023-10-16)')
.title('API Version'),
}),
},
events: {
chargeFailed: {
schema: chargeFailedSchema,
title: 'Charge Failed',
description: 'This event occurs when a charge fails in Stripe.',
},
subscriptionDeleted: {
schema: subscriptionDeletedSchema,
title: 'Subscription Deleted',
description: 'This event occurs when a subscription is canceled/deleted in Stripe.',
},
subscriptionUpdated: {
schema: subscriptionUpdatedSchema,
title: 'Subscription Updated',
description:
'This event occurs when a subscription is updated in Stripe. For example when the subscription is cancelled, but does not terminate immediately cancel_at_period_end goes to true.',
},
subscriptionCreated: {
schema: subscriptionCreatedSchema,
title: 'Subscription Created',
description:
'This event occurs when a subscription is created in Stripe. For example when the subscription is cancelled, but does not terminate immediately cancel_at_period_end goes to true.',
},
invoicePaymentFailed: {
schema: invoicePaymentFailedSchema,
title: 'Invoice Payment Failed',
description: 'This event occurs when an invoice payment fails in Stripe.',
},
paymentIntentFailed: {
schema: paymentIntentFailedSchema,
title: 'Payment Intent Failed',
description: 'This event occurs when a payment intent fails in Stripe.',
},
subscriptionScheduleCreated: {
schema: subscriptionScheduleCreatedSchema,
title: 'Subscription Schedule Created',
description: 'This event occurs when a subscription schedule is created in Stripe.',
},
subscriptionScheduleUpdated: {
schema: subscriptionScheduleUpdatedSchema,
title: 'Subscription Schedule Updated',
description: 'This event occurs when a subscription schedule is updated in Stripe.',
},
},
user: {
tags: {
id: {
title: 'Stripe customer ID',
description: 'The unique identifier for a Stripe customer.',
},
},
},
channels: {},
states: {
oAuthCredentials: {
type: 'integration',
schema: z.object({
accessToken: z.string().secret().title('Access Token').describe('The OAuth access token'),
refreshToken: z.string().secret().title('Refresh Token').describe('The rotating OAuth refresh token'),
expiresAt: z
.string()
.datetime()
.title('Access Token Expires At')
.describe('The timestamp of when the access token expires'),
refreshExpiresAt: z
.string()
.datetime()
.title('Refresh Token Expires At')
.describe('The timestamp of when the refresh token expires'),
scopes: z.array(z.string()).title('Scopes').describe('The scopes granted to the token'),
stripeUserId: z
.string()
.title('Stripe Account ID')
.describe('The Stripe account ID (acct_xxx) the token was issued for'),
livemode: z.boolean().title('Live Mode').describe('Whether the token operates against Stripe live mode'),
}),
},
manualCredentials: {
type: 'integration',
schema: z.object({
apiKey: z
.string()
.secret()
.title('Stripe API Key')
.describe('The secret key or a restricted key from your Stripe account'),
}),
},
stripeIntegrationInfo: {
type: 'integration',
schema: z.object({
stripeWebhookId: z
.string()
.title('Stripe Webhook ID')
.describe('The unique identifier for the Stripe webhook.'),
stripeWebhookSecret: z
.string()
.secret()
.optional()
.title('Stripe Webhook Signing Secret')
.describe('The signing secret returned by Stripe when the webhook endpoint was created.'),
}),
},
},
secrets: {
CLIENT_ID: {
description: 'The client ID of the Stripe OAuth app.',
},
CLIENT_SECRET: {
description: 'The Stripe secret API key (sk_live_/sk_test_) used to authenticate to the OAuth token endpoint.',
},
},
actions: {
createPaymentLink: {
title: 'Create Payment Link',
description: 'Creates a Stripe payment link for a product.',
input: {
schema: createPaymentLinkInputSchema,
},
output: {
schema: createPaymentLinkOutputSchema,
},
},
listProductPrices: {
title: 'List Product Prices',
description: 'Lists all Stripe product prices.',
input: {
schema: listProductPricesInputSchema,
},
output: {
schema: listProductPricesOutputSchema,
},
},
createSubsLink: {
title: 'Create Subscription Payment Link',
description: 'Creates a Stripe payment link for a subscription product.',
input: {
schema: createSubsLinkInputSchema,
},
output: {
schema: createSubsLinkOutputSchema,
},
},
listPaymentLinks: {
title: 'List Payment Links',
description: 'Lists all active Stripe payment links.',
input: {
schema: listPaymentLinksInputSchema,
},
output: {
schema: listPaymentLinksOutputSchema,
},
},
findPaymentLink: {
title: 'Find Payment Link',
description: 'Finds a Stripe payment link by URL.',
input: {
schema: findPaymentLinkInputSchema,
},
output: {
schema: findPaymentLinkOutputSchema,
},
},
deactivatePaymentLink: {
title: 'Deactivate Payment Link',
description: 'Deactivates a Stripe payment link by ID.',
input: {
schema: deactivatePaymentLinkInputSchema,
},
output: {
schema: deactivatePaymentLinkOutputSchema,
},
},
listCustomers: {
title: 'List Customers By Email',
description: 'Lists Stripe customers, optionally filtered by email.',
input: {
schema: listCustomersInputSchema,
},
output: {
schema: listCustomersOutputSchema,
},
},
searchCustomers: {
title: 'Search Customers By Fields',
description: 'Searches Stripe customers by email, name, or phone.',
input: {
schema: searchCustomersInputSchema,
},
output: {
schema: searchCustomersOutputSchema,
},
},
createCustomer: {
title: 'Create Customer',
description: 'Creates a new Stripe customer.',
input: {
schema: createCustomerInputSchema,
},
output: {
schema: createCustomerOutputSchema,
},
},
createOrRetrieveCustomer: {
title: 'Create Or Retrieve Customer',
description: 'Creates a new Stripe customer or retrieves an existing one by email.',
input: {
schema: createOrRetrieveCustomerInputSchema,
},
output: {
schema: createOrRetrieveCustomerOutputSchema,
},
},
retrieveCustomerById: {
title: 'Retrieve Customer By ID',
description: 'Retrieves a Stripe customer by their ID.',
input: {
schema: retrieveCustomerByIdInputSchema,
},
output: {
schema: retrieveCustomerByIdOutputSchema,
},
},
},
__advanced: {
useLegacyZuiTransformer: true,
},
attributes: {
category: 'E-commerce & Payments',
repo: 'botpress',
},
})
+4
View File
@@ -0,0 +1,4 @@
webhookId = to_string!(.webhookId)
webhookUrl = to_string!(.webhookUrl)
"{{ webhookUrl }}/oauth/wizard/start?state={{ webhookId }}"
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@botpresshub/stripe",
"scripts": {
"build": "bp add -y && bp build",
"check:type": "tsc --noEmit",
"check:bplint": "bp lint",
"test": "vitest --run"
},
"private": true,
"dependencies": {
"@botpress/client": "workspace:*",
"@botpress/sdk": "workspace:*",
"stripe": "^13.5.0"
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*"
}
}
@@ -0,0 +1,30 @@
import { createCustomerInputSchema } from '../misc/custom-schemas'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const createCustomer: IntegrationProps['actions']['createCustomer'] = async ({ ctx, client, logger, input }) => {
const validatedInput = createCustomerInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response
try {
const params = {
email: validatedInput.email,
name: validatedInput.name,
phone: validatedInput.phone,
description: validatedInput.description,
payment_method: validatedInput.paymentMethodId,
address: validatedInput.address ? JSON.parse(validatedInput.address) : undefined,
}
const customer = await stripeClient.createCustomer(params)
response = {
customer,
}
logger.forBot().info(`Successful - Create Customer - ${customer.id}`)
} catch (error) {
response = {}
logger.forBot().debug(`'Create Customer' exception ${JSON.stringify(error)}`)
}
return response
}
@@ -0,0 +1,38 @@
import { createOrRetrieveCustomerInputSchema } from '../misc/custom-schemas'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const createOrRetrieveCustomer: IntegrationProps['actions']['createOrRetrieveCustomer'] = async ({
ctx,
client,
logger,
input,
}) => {
const validatedInput = createOrRetrieveCustomerInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response
try {
const customers = await stripeClient.searchCustomers(validatedInput.email)
let customer
if (customers.length === 0) {
const params = {
email: validatedInput.email,
name: validatedInput.name,
phone: validatedInput.phone,
description: validatedInput.description,
payment_method: validatedInput.paymentMethodId,
address: validatedInput.address ? JSON.parse(validatedInput.address) : undefined,
}
customer = await stripeClient.createCustomer(params)
response = { customer }
} else {
response = customers.length === 1 ? { customer: customers[0] } : { customers }
}
logger.forBot().info(`Successful - Create or Retrieve Customer ${customer ? customer.id : ''}`)
} catch (error) {
response = {}
logger.forBot().debug(`'Create or Retrieve Customer' exception ${JSON.stringify(error)}`)
}
return response
}
@@ -0,0 +1,89 @@
import { createPaymentLinkInputSchema } from '../misc/custom-schemas'
import type { ProductBasic } from '../misc/stripe-client'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
const findOrCreateProduct = async (stripeClient: StripeClient, productName: string) => {
const products = await stripeClient.listAllProductsBasic()
let product = products.find((p: ProductBasic) => p.name === productName)
if (!product) {
product = await stripeClient.createProduct(productName)
}
return product
}
const findOrCreatePrice = async (
stripeClient: StripeClient,
productId: string,
unitAmount: number,
currency: string
) => {
const prices = await stripeClient.listPrices(productId)
if (!unitAmount) {
return prices.data[0] || (await stripeClient.createPrice(productId, 0, currency))
}
let price = prices.data.find((p) => p.unit_amount === unitAmount && p.currency === currency)
if (!price) {
price = await stripeClient.createPrice(productId, unitAmount, currency)
}
return price
}
const buildLineItem = (
priceId: string,
quantity: number,
adjustableQuantity: boolean,
adjustableQuantityMaximum: number,
adjustableQuantityMinimum: number
) => {
return {
price: priceId,
quantity: quantity || 1,
adjustable_quantity: {
enabled: adjustableQuantity || false,
maximum: adjustableQuantity ? adjustableQuantityMaximum || 99 : undefined,
minimum: adjustableQuantity ? adjustableQuantityMinimum || 1 : undefined,
},
}
}
export const createPaymentLink: IntegrationProps['actions']['createPaymentLink'] = async ({
ctx,
client,
logger,
input,
}) => {
const validatedInput = createPaymentLinkInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
try {
const product = await findOrCreateProduct(stripeClient, validatedInput.productName)
const price = await findOrCreatePrice(stripeClient, product.id, validatedInput.unit_amount, validatedInput.currency)
const lineItem = buildLineItem(
price.id,
validatedInput.quantity,
validatedInput.adjustableQuantity,
validatedInput.adjustableQuantityMaximum,
validatedInput.adjustableQuantityMinimum
)
const paymentLink = await stripeClient.createPaymentLink(lineItem)
logger.forBot().info(`Successful - Create Payment Link - ${paymentLink.id}`)
return {
id: paymentLink.id,
url: paymentLink.url,
}
} catch (error) {
logger.forBot().debug(`'Create Payment Link' exception ${JSON.stringify(error)}`)
throw error
}
}
@@ -0,0 +1,73 @@
import Stripe from 'stripe'
import { createSubsLinkInputSchema } from '../misc/custom-schemas'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const createSubsLink: IntegrationProps['actions']['createSubsLink'] = async ({ ctx, client, logger, input }) => {
const validatedInput = createSubsLinkInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response
try {
const products = await stripeClient.listAllProductsBasic()
let product = products.find((p) => p.name === validatedInput.productName)
if (!product) {
product = await stripeClient.createProduct(validatedInput.productName)
}
const prices = await stripeClient.listPrices(product.id)
let price
if (!validatedInput.unit_amount) {
price = prices.data.find((p) => p.recurring)
} else {
price = prices.data.find(
(p) => p.unit_amount === validatedInput.unit_amount && p.currency === validatedInput.currency && p.recurring
)
}
const validIntervals = ['day', 'week', 'month', 'year']
const interval: Stripe.PriceCreateParams.Recurring.Interval = validIntervals.includes(
validatedInput.chargingInterval
)
? (validatedInput.chargingInterval as Stripe.PriceCreateParams.Recurring.Interval)
: 'month'
if (!price) {
price = await stripeClient.createSubsPrice(product.id, validatedInput.unit_amount, validatedInput.currency, {
interval,
})
}
const adjustableQuantity = validatedInput.adjustableQuantity || false
const lineItem = {
price: price.id,
quantity: validatedInput.quantity || 1,
adjustable_quantity: {
enabled: adjustableQuantity,
maximum: adjustableQuantity ? validatedInput.adjustableQuantityMaximum || 99 : undefined,
minimum: adjustableQuantity ? validatedInput.adjustableQuantityMinimum || 1 : undefined,
},
}
const subscriptionData = validatedInput.trial_period_days
? {
description: validatedInput.description,
trial_period_days: validatedInput.trial_period_days || 1,
}
: undefined
const paymentLink = await stripeClient.createSubsLink(lineItem, subscriptionData)
response = {
id: paymentLink.id,
url: paymentLink.url,
}
logger.forBot().info(`Successful - Create Subscription Link - ${paymentLink.id}`)
} catch (error) {
response = {}
logger.forBot().debug(`'Create Subscription Link' exception ${JSON.stringify(error)}`)
}
return response
}
@@ -0,0 +1,30 @@
import { deactivatePaymentLinkInputSchema } from '../misc/custom-schemas'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const deactivatePaymentLink: IntegrationProps['actions']['deactivatePaymentLink'] = async ({
ctx,
client,
logger,
input,
}) => {
const validatedInput = deactivatePaymentLinkInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response
try {
const paymentLink = await stripeClient.deactivatePaymentLink(validatedInput.id)
response = {
id: paymentLink.id,
url: paymentLink.url,
active: paymentLink.active,
}
logger.forBot().info(`Successful - Deactivate Payment Link - ${paymentLink?.id}`)
} catch (error) {
response = {}
logger.forBot().debug(`'Deactivate Payment Link' exception ${JSON.stringify(error)}`)
}
return response
}
@@ -0,0 +1,30 @@
import { findPaymentLinkInputSchema } from '../misc/custom-schemas'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const findPaymentLink: IntegrationProps['actions']['findPaymentLink'] = async ({
ctx,
client,
logger,
input,
}) => {
const validatedInput = findPaymentLinkInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response = {}
try {
const paymentLinks = await stripeClient.listAllPaymentLinksBasic()
const paymentLink = paymentLinks.find((link) => link.url === validatedInput.url)
if (paymentLink) {
response = {
id: paymentLink.id,
}
}
logger.forBot().info(`Successful - Find Payment Link - ${paymentLink?.id}`)
} catch (error) {
logger.forBot().debug(`'Find Payment Link' exception ${JSON.stringify(error)}`)
}
return response
}
+25
View File
@@ -0,0 +1,25 @@
import { createCustomer } from './create-customer'
import { createOrRetrieveCustomer } from './create-or-retrieve-customer'
import { createPaymentLink } from './create-paymentlink'
import { createSubsLink } from './create-subslink'
import { deactivatePaymentLink } from './deactivate-paymentlink'
import { findPaymentLink } from './find-paymentlink'
import { listCustomers } from './list-customers'
import { listPaymentLinks } from './list-paymentlinks'
import { listProductPrices } from './list-product-prices'
import { retrieveCustomerById } from './retrieve-customer'
import { searchCustomers } from './search-customers'
export default {
createPaymentLink,
listProductPrices,
createSubsLink,
listPaymentLinks,
findPaymentLink,
deactivatePaymentLink,
listCustomers,
searchCustomers,
createCustomer,
createOrRetrieveCustomer,
retrieveCustomerById,
}
@@ -0,0 +1,34 @@
import type { Customer } from 'src/misc/custom-types'
import { listCustomersInputSchema } from '../misc/custom-schemas'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const listCustomers: IntegrationProps['actions']['listCustomers'] = async ({ ctx, client, logger, input }) => {
const validatedInput = listCustomersInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response
try {
const customers = await stripeClient.listAllCustomerBasic(validatedInput.email || undefined)
const customerByEmails: Record<string, Customer[]> = {}
for (const customer of customers) {
const emailKey = customer.email || 'null_email'
if (customerByEmails[emailKey]) {
customerByEmails[emailKey]?.push(customer)
} else {
customerByEmails[emailKey] = [customer]
}
}
response = {
customers: customerByEmails,
}
logger.forBot().info('Successful - List Customers')
} catch (error) {
response = {}
logger.forBot().debug(`'List Customers' exception ${JSON.stringify(error)}`)
}
return response
}
@@ -0,0 +1,20 @@
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const listPaymentLinks: IntegrationProps['actions']['listPaymentLinks'] = async ({ ctx, client, logger }) => {
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response
try {
const paymentLinks = await stripeClient.listAllPaymentLinksBasic()
response = {
paymentLinks,
}
logger.forBot().info(`Successful - List Payment Links - Total Active: ${paymentLinks.length}`)
} catch (error) {
response = {}
logger.forBot().debug(`'List Payment Links' exception ${JSON.stringify(error)}`)
}
return response
}
@@ -0,0 +1,53 @@
import type { Product } from 'src/misc/custom-types'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const listProductPrices: IntegrationProps['actions']['listProductPrices'] = async ({ ctx, client, logger }) => {
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response
try {
const prices = await stripeClient.listAllPricesBasic(undefined, true)
prices.map((price) => {
return {
productName: (price.product as any).name,
}
})
const products: Record<string, Product> = {}
for (const price of prices) {
if (typeof price.product !== 'string' && 'id' in price.product) {
const product = products[price.product.id]
if (product) {
product.prices.push({
unit_amount: price.unit_amount,
currency: price.currency,
recurring: price.recurring,
})
} else {
products[price.product.id] = {
name: (price.product as any).name,
prices: [
{
unit_amount: price.unit_amount,
currency: price.currency,
recurring: price.recurring,
},
],
}
}
}
}
response = {
products,
}
logger.forBot().info('Successful - List Product Prices')
} catch (error) {
response = {}
logger.forBot().debug(`'List Product Prices' exception ${JSON.stringify(error)}`)
}
return response
}
@@ -0,0 +1,27 @@
import { retrieveCustomerByIdInputSchema } from '../misc/custom-schemas'
import { StripeClient } from '../stripe-api/stripe-client'
import * as bp from '.botpress'
export const retrieveCustomerById: bp.IntegrationProps['actions']['retrieveCustomerById'] = async ({
ctx,
client,
logger,
input,
}) => {
const validatedInput = retrieveCustomerByIdInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
try {
const customer = await stripeClient.retrieveCustomer(validatedInput.id)
if (customer.deleted) {
logger.forBot().info(`Customer not found - Retrieve Customer - ${validatedInput.id}`)
return {}
}
logger.forBot().info(`Successful - Retrieve Customer - ${customer.id}`)
return customer
} catch (error) {
logger.forBot().debug(`'Create or Retrieve Customer' exception ${JSON.stringify(error)}`)
return {}
}
}
@@ -0,0 +1,31 @@
import { searchCustomersInputSchema } from '../misc/custom-schemas'
import type { IntegrationProps } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const searchCustomers: IntegrationProps['actions']['searchCustomers'] = async ({
ctx,
client,
logger,
input,
}) => {
const validatedInput = searchCustomersInputSchema.parse(input)
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
let response
try {
const customers = await stripeClient.searchCustomers(
validatedInput.email,
validatedInput.name,
validatedInput.phone
)
response = {
customers,
}
logger.forBot().info('Successful - Search Customers')
} catch (error) {
response = {}
logger.forBot().debug(`'Search Customers' exception ${JSON.stringify(error)}`)
}
return response
}
@@ -0,0 +1,32 @@
import Stripe from 'stripe'
import { getUserIdFromCustomer } from './utils'
import * as bp from '.botpress'
type Client = bp.Client
type Events = bp.events.Events
type IntegrationLogger = bp.Logger
export const fireChargeFailed = async ({
stripeEvent,
client,
logger,
}: {
stripeEvent: Stripe.ChargeFailedEvent
client: Client
logger: IntegrationLogger
}) => {
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
logger.forBot().debug('Triggering charge failed event')
const payload = {
origin: 'stripe',
userId,
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
} satisfies Events['chargeFailed']
await client.createEvent({
type: 'chargeFailed',
payload,
})
}
+15
View File
@@ -0,0 +1,15 @@
import { fireChargeFailed } from './charge-failed'
import { fireInvoicePaymentFailed } from './invoice-payment-failed'
import { firePaymentIntentFailed } from './payment-intent-failed'
import { fireSubscriptionCreated } from './subscription-created'
import { fireSubscriptionDeleted } from './subscription-deleted'
import { fireSubscriptionUpdated } from './subscription-updated'
export default {
fireChargeFailed,
fireInvoicePaymentFailed,
firePaymentIntentFailed,
fireSubscriptionCreated,
fireSubscriptionDeleted,
fireSubscriptionUpdated,
}
@@ -0,0 +1,32 @@
import Stripe from 'stripe'
import { getUserIdFromCustomer } from './utils'
import * as bp from '.botpress'
type Client = bp.Client
type Events = bp.events.Events
type IntegrationLogger = bp.Logger
export const fireInvoicePaymentFailed = async ({
stripeEvent,
client,
logger,
}: {
stripeEvent: Stripe.InvoicePaymentFailedEvent
client: Client
logger: IntegrationLogger
}) => {
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
logger.forBot().debug('Triggering invoice payment failed event')
const payload = {
origin: 'stripe',
userId,
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
} satisfies Events['invoicePaymentFailed']
await client.createEvent({
type: 'invoicePaymentFailed',
payload,
})
}
@@ -0,0 +1,32 @@
import Stripe from 'stripe'
import { getUserIdFromCustomer } from './utils'
import * as bp from '.botpress'
type Client = bp.Client
type Events = bp.events.Events
type IntegrationLogger = bp.Logger
export const firePaymentIntentFailed = async ({
stripeEvent,
client,
logger,
}: {
stripeEvent: Stripe.PaymentIntentPaymentFailedEvent
client: Client
logger: IntegrationLogger
}) => {
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
logger.forBot().debug('Triggering payment intent failed event')
const payload = {
origin: 'stripe',
userId,
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
} satisfies Events['paymentIntentFailed']
await client.createEvent({
type: 'paymentIntentFailed',
payload,
})
}
@@ -0,0 +1,32 @@
import Stripe from 'stripe'
import { getUserIdFromCustomer } from './utils'
import * as bp from '.botpress'
type Client = bp.Client
type Events = bp.events.Events
type IntegrationLogger = bp.Logger
export const fireSubscriptionCreated = async ({
stripeEvent,
client,
logger,
}: {
stripeEvent: Stripe.CustomerSubscriptionCreatedEvent
client: Client
logger: IntegrationLogger
}) => {
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
logger.forBot().debug('Triggering subscription created event')
const payload = {
origin: 'stripe',
userId,
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
} satisfies Events['subscriptionCreated']
await client.createEvent({
type: 'subscriptionCreated',
payload,
})
}
@@ -0,0 +1,32 @@
import Stripe from 'stripe'
import { getUserIdFromCustomer } from './utils'
import * as bp from '.botpress'
type Client = bp.Client
type Events = bp.events.Events
type IntegrationLogger = bp.Logger
export const fireSubscriptionDeleted = async ({
stripeEvent,
client,
logger,
}: {
stripeEvent: Stripe.CustomerSubscriptionDeletedEvent
client: Client
logger: IntegrationLogger
}) => {
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
logger.forBot().debug('Triggering subscription deleted event')
const payload = {
origin: 'stripe',
userId,
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
} satisfies Events['subscriptionDeleted']
await client.createEvent({
type: 'subscriptionDeleted',
payload,
})
}
@@ -0,0 +1,32 @@
import Stripe from 'stripe'
import { getUserIdFromCustomer } from './utils'
import * as bp from '.botpress'
type Client = bp.Client
type Events = bp.events.Events
type IntegrationLogger = bp.Logger
export const fireSubscriptionScheduleCreated = async ({
stripeEvent,
client,
logger,
}: {
stripeEvent: Stripe.SubscriptionScheduleCreatedEvent
client: Client
logger: IntegrationLogger
}) => {
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
logger.forBot().debug('Triggering subscription schedule created event')
const payload = {
origin: 'stripe',
userId,
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
} satisfies Events['subscriptionScheduleCreated']
await client.createEvent({
type: 'subscriptionScheduleCreated',
payload,
})
}
@@ -0,0 +1,32 @@
import Stripe from 'stripe'
import { getUserIdFromCustomer } from './utils'
import * as bp from '.botpress'
type Client = bp.Client
type Events = bp.events.Events
type IntegrationLogger = bp.Logger
export const fireSubscriptionScheduleUpdated = async ({
stripeEvent,
client,
logger,
}: {
stripeEvent: Stripe.SubscriptionScheduleUpdatedEvent
client: Client
logger: IntegrationLogger
}) => {
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
logger.forBot().debug('Triggering subscription schedule created event')
const payload = {
origin: 'stripe',
userId,
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
} satisfies Events['subscriptionScheduleUpdated']
await client.createEvent({
type: 'subscriptionScheduleUpdated',
payload,
})
}
@@ -0,0 +1,32 @@
import Stripe from 'stripe'
import { getUserIdFromCustomer } from './utils'
import * as bp from '.botpress'
type Client = bp.Client
type Events = bp.events.Events
type IntegrationLogger = bp.Logger
export const fireSubscriptionUpdated = async ({
stripeEvent,
client,
logger,
}: {
stripeEvent: Stripe.CustomerSubscriptionUpdatedEvent
client: Client
logger: IntegrationLogger
}) => {
const userId = await getUserIdFromCustomer(client, stripeEvent.data.object.customer)
logger.forBot().debug('Triggering subscription updated event')
const payload = {
origin: 'stripe',
userId,
data: { type: stripeEvent.type, object: { ...stripeEvent.data.object } },
} satisfies Events['subscriptionUpdated']
await client.createEvent({
type: 'subscriptionUpdated',
payload,
})
}
+37
View File
@@ -0,0 +1,37 @@
import Stripe from 'stripe'
import * as bp from '.botpress'
const NO_USER_ID = 'no user'
type Customer = string | Stripe.Customer | Stripe.DeletedCustomer | null
const isCustomerObject = (obj: unknown): obj is Stripe.Customer => {
return obj !== null && typeof obj === 'object' && 'id' in obj && typeof obj.id === 'string'
}
const isDeletedCustomerObject = (obj: unknown): obj is Stripe.DeletedCustomer => {
return (
obj !== null &&
typeof obj === 'object' &&
'id' in obj &&
typeof obj.id === 'string' &&
'deleted' in obj &&
obj.deleted === true
)
}
const getOrCreateUserFromCustomer = async (client: bp.Client, customer: Customer) => {
if (isDeletedCustomerObject(customer)) {
return await client.getOrCreateUser({ tags: { id: customer.id } })
} else if (isCustomerObject(customer)) {
return await client.getOrCreateUser({ tags: { id: customer.id } })
} else if (typeof customer === 'string') {
return await client.getOrCreateUser({ tags: { id: customer } })
} else {
return
}
}
export const getUserIdFromCustomer = async (client: bp.Client, customer: Customer): Promise<string> => {
const userResponse = await getOrCreateUserFromCustomer(client, customer)
return userResponse?.user.id ?? NO_USER_ID
}
+18
View File
@@ -0,0 +1,18 @@
import { isOAuthWizardUrl } from '@botpress/common/src/oauth-wizard'
import actions from './actions'
import { oauthWizardHandler } from './oauth-wizard'
import { register, unregister, handler } from './setup'
import * as bp from '.botpress'
export default new bp.Integration({
register,
unregister,
actions,
channels: {},
handler: async (props) => {
if (isOAuthWizardUrl(props.req.path)) {
return await oauthWizardHandler(props)
}
return await handler(props)
},
})
@@ -0,0 +1,403 @@
import { z } from '@botpress/sdk'
const partialCustomer = z
.object({
id: z.string().describe('The id of the customer').title('ID'),
email: z.string().nullable().describe('The email of the customer').title('Email'),
name: z.string().nullable().optional().describe('The name of the customer').title('Name'),
description: z.string().nullable().describe('The description of the customer').title('Description'),
phone: z.string().nullable().optional().describe('The phone of the customer').title('Phone'),
address: z.object({}).passthrough().nullable().optional().describe('The addess of the customer').title('Address'),
created: z.number().describe('The creation timestamp (UNIX) of the customer').title('Created'),
delinquent: z.boolean().nullable().optional().describe('If the customer is delinquent').title('Delinquent'),
})
.passthrough()
export const createPaymentLinkInputSchema = z.object({
productName: z
.string()
.placeholder('ex: T-Shirt')
.title('Product Name')
.describe('The name of the product to be sold'),
unit_amount: z
.number()
.min(50, 'Amount must be at least 50 cents ($0.50 USD)')
.optional()
.default(50)
.title('Unit Price')
.describe('The unit price in cents (minimum 50 cents for USD)'),
currency: z
.string()
.optional()
.default('usd')
.title('Currency')
.describe('The currency in which the price will be expressed'),
quantity: z.number().title('Quantity').optional().default(1).describe('The quantity of the product being purchased'),
adjustableQuantity: z
.boolean()
.optional()
.default(false)
.title('Adjustable Quantity')
.describe('Whether or not the quantity can be adjusted'),
adjustableQuantityMaximum: z
.number()
.min(2)
.max(999)
.optional()
.default(99)
.title('Max Quantity')
.describe('The maximum quantity the customer can purchase, up to 999'),
adjustableQuantityMinimum: z
.number()
.min(1)
.max(998)
.optional()
.default(1)
.title('Min Quantity')
.describe('The minimum quantity the customer can purchase'),
})
export const createPaymentLinkOutputSchema = z
.object({
id: z.string().describe('The ID of the created payment link').title('ID'),
url: z.string().describe('The URL of the created payment link').title('URL'),
})
.partial()
export const listProductPricesInputSchema = z.object({})
export const listProductPricesOutputSchema = z
.object({
products: z
.record(
z.object({
name: z.string().describe('The name of the product').title('Name'),
prices: z
.array(
z.object({
unit_amount: z.number().nullable().describe('The unit amount for the product').title('Unit Amount'),
currency: z.string().describe('The currency for the product').title('Currency'),
recurring: z
.object({})
.passthrough()
.nullable()
.optional()
.describe('Recurring price details')
.title('Recurring'),
})
)
.describe('A list of prices for the product')
.title('Prices'),
})
)
.describe('A record of products and their prices')
.title('Products'),
})
.partial()
export const createSubsLinkInputSchema = z.object({
productName: z.string().title('Product Name').describe('The name of the subscription product'),
unit_amount: z.number().title('Unit Amount').optional().default(0).describe('The unit price in cents'),
currency: z
.string()
.title('Currency')
.optional()
.default('usd')
.describe('The currency in which the price is expressed'),
quantity: z
.number()
.title('Quantity')
.optional()
.default(1)
.describe('The quantity of the subscription being purchased'),
adjustableQuantity: z
.boolean()
.title('Adjustable Quantity')
.optional()
.default(false)
.describe('Whether or not the quantity can be adjusted'),
adjustableQuantityMaximum: z
.number()
.title('Max Quantity')
.min(2)
.max(999)
.optional()
.default(99)
.describe('The maximum quantity the customer can purchase, up to 999'),
adjustableQuantityMinimum: z
.number()
.title('Min Quantity')
.min(1)
.max(998)
.optional()
.default(1)
.describe('The minimum quantity the customer can purchase'),
chargingInterval: z // change to .enum(['day', 'week', 'month', 'year'])
.string()
.title('Payment Interval')
.optional()
.default('month')
.describe('The interval at which the customer will be charged'),
trial_period_days: z
.number()
.title('Trial Period Days')
.min(1)
.optional()
.describe('The number of free trial days for the subscription'),
description: z.string().title('Description').optional().describe('A description of the subscription product'),
})
export const createSubsLinkOutputSchema = z
.object({
id: z.string().describe('The ID of the created subscription link').title('ID'),
url: z.string().describe('The URL of the created subscription link').title('URL'),
})
.partial()
export const listPaymentLinksInputSchema = z.object({})
export const listPaymentLinksOutputSchema = z
.object({
paymentLinks: z
.array(
z.object({
id: z.string().describe('The ID of the payment link').title('ID'),
url: z.string().describe('The URL of the payment link').title('URL'),
})
)
.describe('A list of payment links')
.title('Payment Links'),
})
.partial()
export const findPaymentLinkInputSchema = z.object({
url: z
.string()
.title('Payment link')
.describe('The URL of the payment link')
.placeholder('ex: https://buy.stripe.com/test_b0tPr3sS5w3sOm3'),
})
export const findPaymentLinkOutputSchema = z
.object({
id: z.string().describe('The ID of the found payment link').title('ID'),
})
.partial()
export const deactivatePaymentLinkInputSchema = z.object({
id: z
.string()
.title('Payment Link ID')
.describe('The payment link ID to deactivate')
.placeholder('ex: test_aEUdTEdRP95RdvaaEJ'),
})
export const deactivatePaymentLinkOutputSchema = z
.object({
id: z.string().describe('The ID of the deactivated payment link').title('ID'),
url: z.string().describe('The URL of the deactivated payment link').title('URL'),
active: z.boolean().describe('Whether the payment link is active').title('Active'),
})
.partial()
export const listCustomersInputSchema = z.object({
email: z.string().email().max(512).optional().describe('The e-mail of the customer to search for').title('Email'),
})
export const listCustomersOutputSchema = z
.object({
customers: z.record(z.array(partialCustomer)).describe('A record of customers grouped by email').title('Customers'),
})
.partial()
export const searchCustomersInputSchema = z.object({
email: z.string().title('Email').max(512).optional().describe('Search by query on customer emails').title('Email'),
name: z.string().title('Name').optional().describe('Search by query on customer name').title('Name'),
phone: z.string().title('Phone').optional().describe('Search by query on customer phone number').title('Phone'),
})
export const searchCustomersOutputSchema = z
.object({
customers: z.array(partialCustomer).describe('A list of customers matching the search criteria').title('Customers'),
})
.partial()
export const createCustomerInputSchema = z.object({
email: z
.string()
.email()
.max(512)
.title('Email')
.describe('The email of the customer')
.placeholder('johndoe@botpress.com'),
name: z.string().optional().describe('The name of the customer').placeholder('John Doe').title('Name'),
phone: z.string().optional().describe('The phone number of the customer').placeholder('+1234567890').title('Phone'),
description: z
.string()
.optional()
.title('Description')
.describe('A description for the customer')
.placeholder('Customer Description'),
paymentMethodId: z
.string()
.optional()
.title('Payment Method ID')
.describe('The ID of the payment method to attach to the customer')
.placeholder('payment-method-id'),
address: z
.string()
.optional()
.title('Address')
.describe('The address of the customer')
.placeholder('123 Bot Street, Bot City, Botland, 12345'),
})
export const createCustomerOutputSchema = z
.object({
customer: partialCustomer.describe('The created customer object').title('Customer'),
})
.partial()
export const createOrRetrieveCustomerInputSchema = createCustomerInputSchema
export const createOrRetrieveCustomerOutputSchema = z
.object({
customer: partialCustomer.optional().describe('The created or retrieved customer object').title('Customer'),
customers: z
.array(partialCustomer)
.optional()
.describe('A list of customers matching the criteria')
.title('Customers'),
})
.partial()
export const retrieveCustomerByIdInputSchema = z.object({
id: z.string().describe('The ID of the customer to retrieve').title('Customer ID').placeholder('cus_1234567890'),
})
export const retrieveCustomerByIdOutputSchema = z
.object({
id: z.string().describe('The ID of the retrieved customer').title('Customer ID'),
email: z.string().describe('The email of the retrieved customer').title('Customer Email').nullable(),
description: z
.string()
.describe('The description of the retrieved customer')
.title('Customer Description')
.nullable(),
created: z.number().describe('The creation timestamp (UNIX) of the customer').title('Customer Created Timestamp'),
})
.passthrough()
.partial()
const baseSchema = z.object({
origin: z.literal('stripe').describe('The origin of the event trigger').title('Origin'),
userId: z.string().describe('Botpress User ID').title('User ID'),
})
export const chargeFailedSchema = baseSchema.extend({
data: z
.object({
type: z.string().default('charge.failed').describe('The data type').title('Type'),
object: z.object({}).passthrough().describe('The object of the failed charge').title('Charge Object'),
})
.describe('The data to send with the event')
.title('Data'),
})
export const subscriptionCreatedSchema = baseSchema.extend({
data: z
.object({
type: z.string().default('customer.subscription.created').describe('The data type').title('Type'),
object: z
.object({})
.passthrough()
.describe('The object of the created subscription')
.title('Subscription Object'),
})
.describe('The data to send with the event')
.title('Data'),
})
export const subscriptionDeletedSchema = baseSchema.extend({
data: z
.object({
type: z.string().default('customer.subscription.deleted').describe('The data type').title('Type'),
object: z
.object({})
.passthrough()
.describe('The object of the deleted subscription')
.title('Subscription Object'),
})
.describe('The data to send with the event')
.title('Data'),
})
export const subscriptionUpdatedSchema = baseSchema.extend({
data: z
.object({
type: z.string().default('customer.subscription.updated').describe('The data type').title('Type'),
object: z
.object({})
.passthrough()
.describe('The object of the updated subscription')
.title('Subscription Object'),
})
.describe('The data to send with the event')
.title('Data'),
})
export const invoicePaymentFailedSchema = baseSchema.extend({
data: z
.object({
type: z.string().default('invoice.payment_failed').describe('The data type').title('Type'),
object: z
.object({})
.passthrough()
.describe('The object of the invoice whose payment failed')
.title('Invoice Object'),
})
.describe('The data to send with the event')
.title('Data'),
})
export const paymentIntentFailedSchema = baseSchema.extend({
data: z
.object({
type: z.string().default('payment_intent.payment_failed').describe('The data type').title('Type'),
object: z
.object({})
.passthrough()
.describe('The object of the payment intent that failed')
.title('Payment Intent Object'),
})
.describe('The data to send with the event')
.title('Data'),
})
export const subscriptionScheduleCreatedSchema = baseSchema.extend({
data: z
.object({
type: z.string().default('subscription_schedule.created').describe('The data type').title('Type'),
object: z
.object({})
.passthrough()
.describe('The object of the created subscription schedule')
.title('Subscription Schedule Object'),
})
.describe('The data to send with the event')
.title('Data'),
})
export const subscriptionScheduleUpdatedSchema = baseSchema.extend({
data: z
.object({
type: z.string().default('subscription_schedule.updated').describe('The data type').title('Type'),
object: z
.object({})
.passthrough()
.describe('The object of the updated subscription schedule')
.title('Subscription Schedule Object'),
})
.describe('The data to send with the event')
.title('Data'),
})
@@ -0,0 +1,30 @@
type Product = {
name: string
prices: Array<{
unit_amount: number | null
currency: string
recurring?: object | null
}>
}
type Address = {
city: string | null
country: string | null
line1: string | null
line2: string | null
postal_code: string | null
state: string | null
}
type Customer = {
id: string
email: string | null
name?: string | null
description: string | null
phone?: string | null
address?: Address | null
created: number
delinquent?: boolean | null
}
export { Product, Customer }
@@ -0,0 +1,35 @@
import type Stripe from 'stripe'
export type ProductBasic = {
id: string
name: string
}
export type PriceBasic = {
id: string
unit_amount: number | null
currency: string
recurring?: Stripe.Price.Recurring
product: ProductBasic
}
export type CustomerBasic = {
id: string
email: string | null
name: string | null | undefined
description: string | null
phone: string | null | undefined
address: Stripe.Address | null | undefined
created: number
delinquent: boolean | null | undefined
}
export type PaymentLinkBasic = {
id: string
url: string
}
export type WebhookBasic = {
id: string
url: string
}
+11
View File
@@ -0,0 +1,11 @@
import * as bp from '.botpress'
export type Configuration = bp.configuration.Configuration
export type IntegrationProps = bp.IntegrationProps
export type IntegrationCtx = bp.Context
export type RegisterFunction = IntegrationProps['register']
export type UnregisterFunction = IntegrationProps['unregister']
export type Channels = IntegrationProps['channels']
export type Handler = IntegrationProps['handler']
export type Client = bp.Client
@@ -0,0 +1,23 @@
import { isOAuthWizardUrl, getInterstitialUrl, generateRedirection } from '@botpress/common/src/oauth-wizard'
import * as wizard from './wizard'
import * as bp from '.botpress'
export const oauthWizardHandler: bp.IntegrationProps['handler'] = async (props) => {
const { req, logger } = props
if (!isOAuthWizardUrl(req.path)) {
return {
status: 404,
body: 'Invalid OAuth wizard endpoint',
}
}
try {
return await wizard.handler(props)
} catch (thrown: unknown) {
const error = thrown instanceof Error ? thrown : Error(String(thrown))
const errorMessage = 'OAuth wizard error: ' + error.message
logger.forBot().error(errorMessage)
return generateRedirection(getInterstitialUrl(false, errorMessage))
}
}
@@ -0,0 +1,115 @@
import * as oauthWizard from '@botpress/common/src/oauth-wizard'
import { Response, z } from '@botpress/sdk'
import { StripeOAuthClient } from '../stripe-api/stripe-oauth-client'
import * as bp from '.botpress'
type WizardHandler = oauthWizard.WizardStepHandler<bp.HandlerProps>
const _getRedirectUri = () => `${process.env.BP_WEBHOOK_URL}/oauth/wizard/oauth-callback`
const _buildStripeAuthorizeUrl = ({ webhookId }: { webhookId: string }): string => {
const params = new URLSearchParams({
client_id: bp.secrets.CLIENT_ID,
redirect_uri: _getRedirectUri(),
response_type: 'code',
state: webhookId,
})
return `https://marketplace.stripe.com/oauth/v2/authorize?${params.toString()}`
}
const _manualCredentialsSchema = z.object({
apiKey: z
.string()
.secret()
.min(1)
.title('Stripe API Key')
.describe('Your Stripe Secret Key (sk_live_/sk_test_) or a Restricted Key'),
})
const _manualCredentialsForm = {
pageTitle: 'Stripe API Key',
htmlOrMarkdownPageContents:
'Enter a Stripe Secret Key (or Restricted Key). You can create one at <a href="https://dashboard.stripe.com/apikeys" target="_blank">https://dashboard.stripe.com/apikeys</a>.',
schema: _manualCredentialsSchema,
nextStepId: 'save-manual-credentials',
}
export const handler = async (props: bp.HandlerProps): Promise<Response> => {
const wizard = new oauthWizard.OAuthWizardBuilder(props)
.addStep({ id: 'start', handler: _startHandler })
.addStep({ id: 'route-choice', handler: _routeChoiceHandler })
.addStep({ id: 'oauth-redirect', handler: _oauthRedirectHandler })
.addStep({ id: 'oauth-callback', handler: _oauthCallbackHandler })
.addStep({ id: 'get-manual-credentials', handler: _getManualCredentialsHandler })
.addStep({ id: 'save-manual-credentials', handler: _saveManualCredentialsHandler })
.build()
return await wizard.handleRequest()
}
const _startHandler: WizardHandler = ({ responses }) => {
return responses.displayChoices({
pageTitle: 'Stripe Integration Setup',
htmlOrMarkdownPageContents: 'Choose how you would like to configure your Stripe integration:',
choices: [
{ label: 'Connect with OAuth', value: 'oauth' },
{ label: 'Use a Stripe API Key', value: 'manual' },
],
nextStepId: 'route-choice',
})
}
const _routeChoiceHandler: WizardHandler = ({ selectedChoice, responses }) => {
switch (selectedChoice) {
case 'manual':
return responses.redirectToStep('get-manual-credentials')
case 'oauth':
default:
return responses.redirectToStep('oauth-redirect')
}
}
const _oauthRedirectHandler: WizardHandler = async ({ ctx, responses }) => {
return responses.redirectToExternalUrl(_buildStripeAuthorizeUrl({ webhookId: ctx.webhookId }))
}
const _oauthCallbackHandler: WizardHandler = async ({ ctx, client, logger, responses, query }) => {
const code = query.get('code')
if (!code) {
return responses.endWizard({ success: false, errorMessage: 'Stripe did not return an authorization code' })
}
const state = query.get('state')
if (!state || state !== ctx.webhookId) {
return responses.endWizard({ success: false, errorMessage: 'Invalid OAuth state parameter' })
}
const oauth = new StripeOAuthClient({ client, ctx, logger })
await oauth.requestShortLivedCredentials.fromAuthorizationCode(code)
return responses.endWizard({ success: true })
}
const _getManualCredentialsHandler: WizardHandler = ({ responses }) => {
return responses.displayForm(_manualCredentialsForm)
}
const _saveManualCredentialsHandler: WizardHandler = async ({ ctx, client, logger, formValues, responses }) => {
if (!formValues) {
return responses.redirectToStep('get-manual-credentials')
}
const parsed = _manualCredentialsSchema.safeParse(formValues)
if (!parsed.success) {
return responses.displayForm({
..._manualCredentialsForm,
errors: parsed.error,
previousValues: formValues as z.input<typeof _manualCredentialsSchema>,
})
}
const oauth = new StripeOAuthClient({ client, ctx, logger })
await oauth.saveManualApiKey(parsed.data.apiKey)
return responses.endWizard({ success: true })
}
+129
View File
@@ -0,0 +1,129 @@
import { fireChargeFailed } from 'src/events/charge-failed'
import { fireInvoicePaymentFailed } from 'src/events/invoice-payment-failed'
import { firePaymentIntentFailed } from 'src/events/payment-intent-failed'
import { fireSubscriptionCreated } from 'src/events/subscription-created'
import { fireSubscriptionDeleted } from 'src/events/subscription-deleted'
import { fireSubscriptionScheduleCreated } from 'src/events/subscription-schedule-created'
import { fireSubscriptionScheduleUpdated } from 'src/events/subscription-schedule-updated'
import { fireSubscriptionUpdated } from 'src/events/subscription-updated'
import Stripe from 'stripe'
import type { Handler } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
import { ENABLED_EVENTS } from './register'
// Stand-in Stripe instance only used for webhook signature verification (no API calls).
// @ts-ignore
const _signatureVerifier = new Stripe('placeholder', { apiVersion: '2023-08-16' })
const _recreateWebhookWithSecret = async ({
client,
ctx,
logger,
priorWebhookId,
}: {
client: Parameters<Handler>[0]['client']
ctx: Parameters<Handler>[0]['ctx']
logger: Parameters<Handler>[0]['logger']
priorWebhookId?: string
}) => {
if (!process.env.BP_WEBHOOK_URL) {
throw new Error('BP_WEBHOOK_URL is not configured')
}
const stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
const { id: stripeWebhookId, secret: stripeWebhookSecret } = await stripeClient.createWebhookEndpointWithSecret({
url: `${process.env.BP_WEBHOOK_URL}/${ctx.webhookId}`,
enabled_events: ENABLED_EVENTS,
})
await client.setState({
type: 'integration',
id: ctx.integrationId,
name: 'stripeIntegrationInfo',
payload: { stripeWebhookId, stripeWebhookSecret },
})
if (priorWebhookId) {
try {
await stripeClient.deleteWebhook(priorWebhookId)
} catch (error) {
logger.forBot().warn(`Failed to delete legacy Stripe webhook ${priorWebhookId}`, error)
}
}
}
export const handler: Handler = async ({ req, client, ctx, logger }) => {
if (!req.body) {
logger.forBot().warn('Stripe webhook handler received an empty body')
return { status: 400, body: 'Empty body' }
}
const sigHeader = req.headers['stripe-signature']
if (!sigHeader) {
logger.forBot().warn('Stripe webhook missing stripe-signature header')
return { status: 400, body: 'Missing stripe-signature header' }
}
const integrationInfo = await client
.getState({ type: 'integration', id: ctx.integrationId, name: 'stripeIntegrationInfo' })
.then(({ state }) => state.payload)
.catch(() => undefined)
const webhookSecret = integrationInfo?.stripeWebhookSecret
if (!webhookSecret) {
try {
await _recreateWebhookWithSecret({
client,
ctx,
logger,
priorWebhookId: integrationInfo?.stripeWebhookId,
})
logger.forBot().warn('Recreated Stripe webhook because the stored signing secret was missing')
return { status: 202, body: 'Stripe webhook recreated; delivery skipped because it cannot be verified' }
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
logger.forBot().error(`No Stripe webhook signing secret found and migration failed: ${message}`)
return { status: 500, body: 'Webhook signing secret not configured' }
}
}
let stripeEvent: Stripe.Event
try {
stripeEvent = _signatureVerifier.webhooks.constructEvent(req.body, sigHeader, webhookSecret)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
logger.forBot().warn(`Stripe webhook signature verification failed: ${message}`)
return { status: 400, body: 'Invalid signature' }
}
switch (stripeEvent.type) {
case 'charge.failed':
await fireChargeFailed({ stripeEvent, client, logger })
break
case 'invoice.payment_failed':
await fireInvoicePaymentFailed({ stripeEvent, client, logger })
break
case 'payment_intent.payment_failed':
await firePaymentIntentFailed({ stripeEvent, client, logger })
break
case 'customer.subscription.created':
await fireSubscriptionCreated({ stripeEvent, client, logger })
break
case 'customer.subscription.deleted':
await fireSubscriptionDeleted({ stripeEvent, client, logger })
break
case 'customer.subscription.updated':
await fireSubscriptionUpdated({ stripeEvent, client, logger })
break
case 'subscription_schedule.created':
await fireSubscriptionScheduleCreated({ stripeEvent, client, logger })
break
case 'subscription_schedule.updated':
await fireSubscriptionScheduleUpdated({ stripeEvent, client, logger })
break
default:
logger.forBot().warn(`Unhandled Stripe event type ${stripeEvent.type}`)
}
return
}
+3
View File
@@ -0,0 +1,3 @@
export { register } from './register'
export { unregister } from './unregister'
export { handler } from './handler'
+63
View File
@@ -0,0 +1,63 @@
import * as sdk from '@botpress/sdk'
import Stripe from 'stripe'
import type { RegisterFunction } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const ENABLED_EVENTS: Stripe.WebhookEndpointCreateParams.EnabledEvent[] = [
'charge.failed',
'customer.subscription.deleted',
'customer.subscription.updated',
'customer.subscription.created',
'invoice.payment_failed',
'payment_intent.payment_failed',
'subscription_schedule.created',
'subscription_schedule.updated',
]
export const register: RegisterFunction = async ({ ctx, client, webhookUrl, logger }) => {
let stripeClient: StripeClient
try {
stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
throw new sdk.RuntimeError(`Failed to load Stripe credentials. Re-run the setup wizard. (${message})`)
}
let accountId: string
try {
const account = await stripeClient.retrieveAccount()
accountId = account.id
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
throw new sdk.RuntimeError(`Failed to connect to Stripe. (${message})`)
}
await client.configureIntegration({ identifier: accountId })
const prior = await client
.getState({ type: 'integration', id: ctx.integrationId, name: 'stripeIntegrationInfo' })
.then(({ state }) => state.payload)
.catch(() => undefined)
const { id: stripeWebhookId, secret: stripeWebhookSecret } = await stripeClient.createWebhookEndpointWithSecret({
url: webhookUrl,
enabled_events: ENABLED_EVENTS,
})
await client.setState({
type: 'integration',
id: ctx.integrationId,
name: 'stripeIntegrationInfo',
payload: { stripeWebhookId, stripeWebhookSecret },
})
if (prior?.stripeWebhookId && prior.stripeWebhookId !== stripeWebhookId) {
try {
await stripeClient.deleteWebhook(prior.stripeWebhookId)
} catch (error) {
logger.forBot().warn(`Failed to delete prior Stripe webhook ${prior.stripeWebhookId}`, error)
}
}
logger.forBot().info('Connection to Stripe successful')
}
@@ -0,0 +1,26 @@
import type { UnregisterFunction } from '../misc/types'
import { StripeClient } from '../stripe-api/stripe-client'
export const unregister: UnregisterFunction = async ({ ctx, client, logger }) => {
let stripeClient: StripeClient
try {
stripeClient = await StripeClient.createFromStates({ client, ctx, logger })
} catch {
logger.forBot().warn('No Stripe credentials available; skipping webhook teardown')
return
}
const stateStripeIntegrationInfo = await client
.getState({ id: ctx.integrationId, name: 'stripeIntegrationInfo', type: 'integration' })
.catch(() => undefined)
if (!stateStripeIntegrationInfo) return
const { stripeWebhookId } = stateStripeIntegrationInfo.state.payload
if (stripeWebhookId) {
const response = await stripeClient.deleteWebhook(stripeWebhookId)
if (response.deleted) {
logger.forBot().info(`Webhook successfully deleted - ${response.id}`)
}
}
}
@@ -0,0 +1,258 @@
import Stripe from 'stripe'
import { StripeOAuthClient } from './stripe-oauth-client'
import * as bp from '.botpress'
type CreateProps = {
client: bp.Client
ctx: bp.Context
logger: bp.Logger
}
type LegacyConfiguration = bp.Context['configuration'] & {
apiKey?: string
}
export class StripeClient {
protected _stripe: Stripe
public constructor(apiKey: string, apiVersion?: string) {
// @ts-ignore
this._stripe = new Stripe(apiKey, { apiVersion: apiVersion || '2023-08-16' })
}
public static async createFromStates({ client, ctx, logger }: CreateProps): Promise<StripeClient> {
const oauth = new StripeOAuthClient({ client, ctx, logger })
// essential for old API key support
let accessToken: string
try {
accessToken = (await oauth.getAuthState()).accessToken
} catch (error) {
const legacyApiKey = (ctx.configuration as LegacyConfiguration).apiKey
if (!legacyApiKey) {
throw error
}
await oauth.saveManualApiKey(legacyApiKey)
accessToken = legacyApiKey
}
return new StripeClient(accessToken, ctx.configuration.apiVersion)
}
public async retrieveAccount(): Promise<Stripe.Account> {
return await this._stripe.accounts.retrieve()
}
public async createProduct(name: string) {
const product = await this._stripe.products.create({ name })
return product
}
public async listProducts(startingAfter?: string) {
return await this._stripe.products.list({
active: true,
limit: 100,
starting_after: startingAfter,
})
}
public async listAllProductsBasic(startingAfter?: string) {
let products = await this.listProducts(startingAfter)
const productsBasic = products.data.map((product) => ({ id: product.id, name: product.name }))
while (products.has_more) {
products = await this.listProducts(productsBasic[productsBasic.length - 1]?.id)
for (const product of products.data) {
productsBasic.push({ id: product.id, name: product.name })
}
}
return productsBasic
}
public async createPrice(product: string, unit_amount: number, currency: string) {
return await this._stripe.prices.create({ product, unit_amount, currency })
}
public async createSubsPrice(
product: string,
unit_amount: number,
currency: string,
recurring: Stripe.PriceCreateParams.Recurring
) {
return await this._stripe.prices.create({ product, unit_amount, currency, recurring })
}
public async listPrices(productId?: string, isExpand: boolean = false, startingAfter?: string) {
return await this._stripe.prices.list({
active: true,
limit: 100,
product: productId,
expand: isExpand ? ['data.product'] : undefined,
starting_after: startingAfter,
})
}
public async listAllPricesBasic(productId?: string, isExpand: boolean = false, startingAfter?: string) {
let prices = await this.listPrices(productId, isExpand, startingAfter)
const pricesBasic = prices.data.map((price) => ({
id: price.id,
unit_amount: price.unit_amount,
currency: price.currency,
recurring: price.recurring || undefined,
product: {
id: (price.product as Stripe.Product).id,
name: (price.product as Stripe.Product).name,
},
}))
while (prices.has_more) {
prices = await this.listPrices(productId, isExpand, pricesBasic[pricesBasic.length - 1]?.id)
for (const price of prices.data) {
pricesBasic.push({
id: price.id,
unit_amount: price.unit_amount,
currency: price.currency,
recurring: price.recurring || undefined,
product: {
id: (price.product as Stripe.Product).id,
name: (price.product as Stripe.Product).name,
},
})
}
}
return pricesBasic
}
public async createPaymentLink(lineItem: Stripe.PaymentLinkCreateParams.LineItem) {
return await this._stripe.paymentLinks.create({ line_items: [lineItem] })
}
public async listPaymentLink(startingAfter?: string) {
return await this._stripe.paymentLinks.list({
active: true,
limit: 100,
starting_after: startingAfter,
})
}
public async listAllPaymentLinksBasic(startingAfter?: string) {
let paymentLinks = await this.listPaymentLink(startingAfter)
const paymentLinksBasic = paymentLinks.data.map((paymentLink) => ({
id: paymentLink.id,
url: paymentLink.url,
}))
while (paymentLinks.has_more) {
paymentLinks = await this.listPaymentLink(paymentLinksBasic[paymentLinksBasic.length - 1]?.id)
for (const paymentLink of paymentLinks.data) {
paymentLinksBasic.push({ id: paymentLink.id, url: paymentLink.url })
}
}
return paymentLinksBasic
}
public async createSubsLink(
lineItem: Stripe.PaymentLinkCreateParams.LineItem,
subscriptionData: Stripe.PaymentLinkCreateParams.SubscriptionData | undefined
) {
return await this._stripe.paymentLinks.create({
line_items: [lineItem],
subscription_data: subscriptionData,
})
}
public async deactivatePaymentLink(paymentLinkId: string) {
return this._stripe.paymentLinks.update(paymentLinkId, { active: false })
}
public async listCustomer(email?: string, startingAfter?: string) {
return await this._stripe.customers.list({ email, limit: 100, starting_after: startingAfter })
}
public async listAllCustomerBasic(email?: string, startingAfter?: string) {
let customers = await this.listCustomer(email, startingAfter)
const customersBasic = customers.data.map((customer) => ({
id: customer.id,
email: customer.email,
name: customer.name,
description: customer.description,
phone: customer.phone,
address: customer.address,
created: customer.created,
delinquent: customer.delinquent,
}))
while (customers.has_more) {
customers = await this.listCustomer(email, customersBasic[customersBasic.length - 1]?.id)
for (const customer of customers.data) {
customersBasic.push({
id: customer.id,
email: customer.email,
name: customer.name,
description: customer.description,
phone: customer.phone,
address: customer.address,
created: customer.created,
delinquent: customer.delinquent,
})
}
}
return customersBasic
}
public async searchCustomers(email?: string, name?: string, phone?: string) {
const queryParts: string[] = []
if (email) queryParts.push(`email~'${email}'`)
if (name) queryParts.push(`name~'${name}'`)
if (phone) queryParts.push(`phone~'${phone}'`)
const query = queryParts.join(' AND ')
const limit = 100
let response = await this._stripe.customers.search({ query, limit })
const customers = response.data
while (response.has_more) {
const page = response.next_page || undefined
response = await this._stripe.customers.search({ query, limit, page })
customers.push(...response.data)
}
return customers
}
public async createCustomer(params: Stripe.CustomerCreateParams) {
return await this._stripe.customers.create(params)
}
public async createWebhook(webhookData: Stripe.WebhookEndpointCreateParams) {
return await this._stripe.webhookEndpoints.create(webhookData)
}
public async listWebhooks(startingAfter?: string) {
return await this._stripe.webhookEndpoints.list({ limit: 100, starting_after: startingAfter })
}
public async listAllWebhooksBasic(startingAfter?: string) {
let webhooks = await this.listWebhooks(startingAfter)
const webhooksBasic = webhooks.data.map((webhook) => ({ id: webhook.id, url: webhook.url }))
while (webhooks.has_more) {
webhooks = await this.listWebhooks(webhooksBasic[webhooksBasic.length - 1]?.id)
for (const webhook of webhooks.data) {
webhooksBasic.push({ id: webhook.id, url: webhook.url })
}
}
return webhooksBasic
}
public async createWebhookEndpointWithSecret(
webhookData: Stripe.WebhookEndpointCreateParams
): Promise<{ id: string; secret: string }> {
const webhook = await this.createWebhook(webhookData)
if (!webhook.secret) {
throw new Error('Stripe did not return a webhook signing secret on creation')
}
return { id: webhook.id, secret: webhook.secret }
}
public async deleteWebhook(webhookId: string) {
return await this._stripe.webhookEndpoints.del(webhookId)
}
public async retrieveCustomer(id: string) {
return await this._stripe.customers.retrieve(id)
}
}
@@ -0,0 +1,258 @@
import * as sdk from '@botpress/sdk'
import * as bp from '.botpress'
type StripeTokenResponse = {
access_token: string
refresh_token: string
token_type: string
stripe_user_id: string
stripe_publishable_key?: string
livemode: boolean
scope?: string
expires_in?: number
refresh_expires_in?: number
}
type PrivateAuthState = {
readonly accessToken: {
readonly expiresAt: Date
readonly token: string
}
readonly refreshToken: {
readonly expiresAt: Date
readonly token: string
}
readonly scopes: string[]
readonly stripeUserId: string
readonly livemode: boolean
}
type PublicAuthState = {
readonly accessToken: string
readonly scopes: string[]
readonly stripeUserId?: string
readonly livemode?: boolean
}
const STRIPE_TOKEN_URL = 'https://api.stripe.com/v1/oauth/token'
const MINIMUM_TOKEN_VALIDITY_SECONDS = 3_600
const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 3_600
const DEFAULT_REFRESH_TOKEN_TTL_SECONDS = 365 * 24 * 60 * 60
export class StripeOAuthClient {
private readonly _client: bp.Client
private readonly _ctx: bp.Context
private readonly _logger: bp.Logger
private readonly _clientSecret: string
private _currentAuthState: PrivateAuthState | undefined = undefined
public constructor({
ctx,
client,
logger,
clientSecretOverride,
}: {
client: bp.Client
ctx: bp.Context
logger: bp.Logger
clientSecretOverride?: string
}) {
this._clientSecret = clientSecretOverride ?? bp.secrets.CLIENT_SECRET
this._client = client
this._ctx = ctx
this._logger = logger
}
public async getAuthState(): Promise<PublicAuthState> {
const manual = await this._getManualCredentialsState()
if (manual && manual.apiKey !== '') {
return { accessToken: manual.apiKey, scopes: [] }
}
await this._refreshAuthStateIfNeeded()
if (!this._currentAuthState) {
throw new sdk.RuntimeError('No Stripe credentials found. Re-run the integration setup wizard.')
}
return {
accessToken: this._currentAuthState.accessToken.token,
scopes: this._currentAuthState.scopes,
stripeUserId: this._currentAuthState.stripeUserId,
livemode: this._currentAuthState.livemode,
}
}
public readonly requestShortLivedCredentials = {
fromAuthorizationCode: async (authorizationCode: string) => {
this._logger.forBot().debug('Exchanging Stripe authorization code for credentials...')
const response = await this._postToken({ grant_type: 'authorization_code', code: authorizationCode })
this._currentAuthState = this._parseStripeTokenResponse(response)
await this._saveOAuthCredentials()
await this._clearManualCredentials()
this._logger.forBot().debug('Successfully exchanged Stripe authorization code')
},
fromRefreshToken: async (refreshToken: string) => {
this._logger.forBot().debug('Refreshing Stripe access token...')
const response = await this._postToken({ grant_type: 'refresh_token', refresh_token: refreshToken })
this._currentAuthState = this._parseStripeTokenResponse(response)
await this._saveOAuthCredentials()
this._logger.forBot().debug('Successfully refreshed Stripe access token')
},
}
public async saveManualApiKey(apiKey: string): Promise<void> {
await this._client.setState({
type: 'integration',
name: 'manualCredentials',
id: this._ctx.integrationId,
payload: { apiKey },
})
await this._clearOAuthCredentials()
}
private async _postToken(body: Record<string, string>): Promise<StripeTokenResponse> {
const basicAuth = Buffer.from(`${this._clientSecret}:`).toString('base64')
const response = await fetch(STRIPE_TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${basicAuth}`,
},
body: new URLSearchParams(body).toString(),
})
if (!response.ok) {
const errorBody = await response.text()
this._logger.forBot().error(`Stripe token endpoint returned ${response.status}: ${errorBody}`)
throw new sdk.RuntimeError(`Stripe token request failed (${response.status})`)
}
return (await response.json()) as StripeTokenResponse
}
private async _refreshAuthStateIfNeeded(): Promise<void> {
if (this._isTokenStillValid()) {
return
}
const credentials = await this._getOAuthCredentialsState()
if (!credentials) {
throw new sdk.RuntimeError('No Stripe credentials found. Re-run the integration setup wizard.')
}
await this._refreshAuth(credentials)
}
private _isTokenStillValid() {
return this._currentAuthState && this._currentAuthState.accessToken.expiresAt > this._getMinExpiryDate()
}
private _getMinExpiryDate() {
return new Date(Date.now() + MINIMUM_TOKEN_VALIDITY_SECONDS * 1000)
}
private async _getManualCredentialsState() {
return this._client
.getState({ type: 'integration', id: this._ctx.integrationId, name: 'manualCredentials' })
.then(({ state }) => state.payload)
.catch(() => undefined)
}
private async _getOAuthCredentialsState() {
return this._client
.getState({ type: 'integration', id: this._ctx.integrationId, name: 'oAuthCredentials' })
.then(({ state }) => state.payload)
.catch(() => undefined)
}
private async _refreshAuth(credentials: bp.states.oAuthCredentials.OAuthCredentials['payload']) {
const accessTokenExpiresAt = new Date(credentials.expiresAt)
const refreshTokenExpiresAt = new Date(credentials.refreshExpiresAt)
if (refreshTokenExpiresAt <= new Date()) {
throw new sdk.RuntimeError('Stripe refresh token has expired. Please re-run the integration setup wizard.')
}
if (accessTokenExpiresAt > new Date()) {
this._currentAuthState = {
accessToken: { expiresAt: accessTokenExpiresAt, token: credentials.accessToken },
refreshToken: { expiresAt: refreshTokenExpiresAt, token: credentials.refreshToken },
scopes: credentials.scopes,
stripeUserId: credentials.stripeUserId,
livemode: credentials.livemode,
}
return
}
await this.requestShortLivedCredentials.fromRefreshToken(credentials.refreshToken)
}
private _parseStripeTokenResponse(response: StripeTokenResponse): PrivateAuthState {
if (!response.access_token || !response.refresh_token || !response.stripe_user_id) {
this._logger.forBot().error('Stripe OAuth response is missing required fields')
throw new sdk.RuntimeError('Stripe OAuth response is missing required fields')
}
const now = Date.now()
const accessTtl = response.expires_in ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS
const refreshTtl = response.refresh_expires_in ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS
return {
accessToken: { expiresAt: new Date(now + accessTtl * 1000), token: response.access_token },
refreshToken: { expiresAt: new Date(now + refreshTtl * 1000), token: response.refresh_token },
scopes: response.scope ? response.scope.split(' ') : [],
stripeUserId: response.stripe_user_id,
livemode: response.livemode ?? false,
}
}
private async _clearManualCredentials() {
await this._client.setState({
type: 'integration',
name: 'manualCredentials',
id: this._ctx.integrationId,
payload: { apiKey: '' },
})
}
private async _clearOAuthCredentials() {
const epoch = new Date(0).toISOString()
await this._client.setState({
type: 'integration',
name: 'oAuthCredentials',
id: this._ctx.integrationId,
payload: {
accessToken: '',
refreshToken: '',
expiresAt: epoch,
refreshExpiresAt: epoch,
scopes: [],
stripeUserId: '',
livemode: false,
},
})
}
private async _saveOAuthCredentials() {
if (!this._currentAuthState) {
throw new sdk.RuntimeError('No credentials to save')
}
await this._client.setState({
type: 'integration',
name: 'oAuthCredentials',
id: this._ctx.integrationId,
payload: {
accessToken: this._currentAuthState.accessToken.token,
refreshToken: this._currentAuthState.refreshToken.token,
expiresAt: this._currentAuthState.accessToken.expiresAt.toISOString(),
refreshExpiresAt: this._currentAuthState.refreshToken.expiresAt.toISOString(),
scopes: this._currentAuthState.scopes,
stripeUserId: this._currentAuthState.stripeUserId,
livemode: this._currentAuthState.livemode,
},
})
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"paths": { "*": ["./*"] },
"outDir": "dist",
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
}
+2
View File
@@ -0,0 +1,2 @@
import config from '../../vitest.config'
export default config