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,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,
},
})
}
}