diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..b512c09d4 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/apps/builder/next.config.mjs b/apps/builder/next.config.mjs index 6bcc63c63..def99c9ce 100644 --- a/apps/builder/next.config.mjs +++ b/apps/builder/next.config.mjs @@ -21,7 +21,7 @@ const injectViewerUrlIfVercelPreview = (val) => { process.env.VERCEL_BUILDER_PROJECT_NAME, process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME ) - if (process.env.NEXT_PUBLIC_CHAT_API_URL.includes('{{pr_id}}')) + if (process.env.NEXT_PUBLIC_CHAT_API_URL?.includes('{{pr_id}}')) process.env.NEXT_PUBLIC_CHAT_API_URL = process.env.NEXT_PUBLIC_CHAT_API_URL.replace( '{{pr_id}}', @@ -56,12 +56,14 @@ const nextConfig = { if (nextRuntime === 'edge') { config.resolve.alias['minio'] = false config.resolve.alias['got'] = false + config.resolve.alias['qrcode'] = false return config } // These packages are imports from the integrations definition files that can be ignored for the client. config.resolve.alias['minio'] = false config.resolve.alias['got'] = false config.resolve.alias['openai'] = false + config.resolve.alias['qrcode'] = false return config }, headers: async () => { diff --git a/apps/builder/package.json b/apps/builder/package.json index 8d377fc7c..56ce27422 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -67,7 +67,7 @@ "framer-motion": "10.3.0", "google-auth-library": "8.9.0", "google-spreadsheet": "4.1.1", - "got": "12.6.0", + "ky": "1.2.3", "immer": "10.0.2", "jsonwebtoken": "9.0.1", "libphonenumber-js": "1.10.37", diff --git a/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts b/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts index c77689e1e..5d7315ecf 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/api/getResultExample.ts @@ -32,7 +32,6 @@ export const getResultExample = authenticatedProcedure }) ) .query(async ({ input: { typebotId, blockId }, ctx: { user } }) => { - console.log('user', user) const typebot = (await prisma.typebot.findFirst({ where: canReadTypebots(typebotId, user), select: { diff --git a/apps/builder/src/features/blocks/integrations/webhook/helpers/convertVariablesForTestToVariables.ts b/apps/builder/src/features/blocks/integrations/webhook/helpers/convertVariablesForTestToVariables.ts index b150e0c42..812697659 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/helpers/convertVariablesForTestToVariables.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/helpers/convertVariablesForTestToVariables.ts @@ -17,7 +17,7 @@ export const convertVariablesForTestToVariables = ( ) as Variable return { ...variable, value: parseVariableValue(variableForTest.value) } }, {}), - ] + ].filter((v) => v.value) } const parseVariableValue = (value: string | undefined): string | string[] => { diff --git a/apps/builder/src/features/blocks/integrations/webhook/queries/executeWebhookQuery.ts b/apps/builder/src/features/blocks/integrations/webhook/queries/executeWebhookQuery.ts index 0ec6f3971..fcc685680 100644 --- a/apps/builder/src/features/blocks/integrations/webhook/queries/executeWebhookQuery.ts +++ b/apps/builder/src/features/blocks/integrations/webhook/queries/executeWebhookQuery.ts @@ -1,6 +1,5 @@ import { Variable, HttpResponse } from '@typebot.io/schemas' import { sendRequest } from '@typebot.io/lib' -import { env } from '@typebot.io/env' export const executeWebhook = ( typebotId: string, @@ -8,7 +7,7 @@ export const executeWebhook = ( { blockId }: { blockId: string } ) => sendRequest({ - url: `${env.NEXT_PUBLIC_VIEWER_URL[0]}/api/typebots/${typebotId}/blocks/${blockId}/executeWebhook`, + url: `/api/typebots/${typebotId}/blocks/${blockId}/testWebhook`, method: 'POST', body: { variables, diff --git a/apps/builder/src/features/blocks/integrations/zemanticAi/api/listProjects.ts b/apps/builder/src/features/blocks/integrations/zemanticAi/api/listProjects.ts index cabdc8dea..e764ee869 100644 --- a/apps/builder/src/features/blocks/integrations/zemanticAi/api/listProjects.ts +++ b/apps/builder/src/features/blocks/integrations/zemanticAi/api/listProjects.ts @@ -5,7 +5,7 @@ import { z } from 'zod' import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' import { ZemanticAiCredentials } from '@typebot.io/schemas/features/blocks/integrations/zemanticAi' -import got from 'got' +import ky from 'ky' export const listProjects = authenticatedProcedure .input( @@ -58,7 +58,7 @@ export const listProjects = authenticatedProcedure const url = 'https://api.zemantic.ai/v1/projects' try { - const response = await got + const response = await ky .get(url, { headers: { Authorization: `Bearer ${data.apiKey}`, diff --git a/apps/builder/src/features/customDomains/api/createCustomDomain.ts b/apps/builder/src/features/customDomains/api/createCustomDomain.ts index a9d7585e1..960b50da3 100644 --- a/apps/builder/src/features/customDomains/api/createCustomDomain.ts +++ b/apps/builder/src/features/customDomains/api/createCustomDomain.ts @@ -3,7 +3,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' import { customDomainSchema } from '@typebot.io/schemas/features/customDomains' -import got, { HTTPError } from 'got' +import ky, { HTTPError } from 'ky' import { env } from '@typebot.io/env' import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden' import { trackEvents } from '@typebot.io/telemetry/trackEvents' @@ -61,12 +61,12 @@ export const createCustomDomain = authenticatedProcedure try { await createDomainOnVercel(name) } catch (err) { - console.log(err) - if (err instanceof HTTPError && err.response.statusCode !== 409) + if (err instanceof HTTPError && err.response.status !== 409) { throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to create custom domain on Vercel', }) + } } const customDomain = await prisma.customDomain.create({ @@ -91,8 +91,12 @@ export const createCustomDomain = authenticatedProcedure }) const createDomainOnVercel = (name: string) => - got.post({ - url: `https://api.vercel.com/v10/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains?teamId=${env.VERCEL_TEAM_ID}`, - headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` }, - json: { name }, - }) + ky.post( + `https://api.vercel.com/v10/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains?teamId=${env.VERCEL_TEAM_ID}`, + { + headers: { + authorization: `Bearer ${env.VERCEL_TOKEN}`, + }, + json: { name }, + } + ) diff --git a/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts b/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts index ba07715a6..bfc290195 100644 --- a/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts +++ b/apps/builder/src/features/customDomains/api/deleteCustomDomain.ts @@ -2,7 +2,7 @@ import prisma from '@typebot.io/lib/prisma' import { authenticatedProcedure } from '@/helpers/server/trpc' import { TRPCError } from '@trpc/server' import { z } from 'zod' -import got from 'got' +import ky from 'ky' import { env } from '@typebot.io/env' import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden' @@ -63,7 +63,9 @@ export const deleteCustomDomain = authenticatedProcedure }) const deleteDomainOnVercel = (name: string) => - got.delete({ - url: `https://api.vercel.com/v9/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`, - headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` }, - }) + ky.delete( + `https://api.vercel.com/v9/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`, + { + headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` }, + } + ) diff --git a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx index 8ce3a9c83..54748a269 100644 --- a/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx +++ b/apps/builder/src/features/publish/components/embeds/modals/WhatsAppModal/WhatsAppCredentialsModal.tsx @@ -377,6 +377,7 @@ const SystemUserToken = ({ Copy and paste the generated token: setToken(val.trim())} diff --git a/apps/builder/src/features/whatsapp/getPhoneNumber.ts b/apps/builder/src/features/whatsapp/getPhoneNumber.ts index 06adb8e03..2b842a3a8 100644 --- a/apps/builder/src/features/whatsapp/getPhoneNumber.ts +++ b/apps/builder/src/features/whatsapp/getPhoneNumber.ts @@ -1,6 +1,6 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' -import got from 'got' +import ky from 'ky' import prisma from '@typebot.io/lib/prisma' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' import { TRPCError } from '@trpc/server' @@ -22,16 +22,13 @@ export const getPhoneNumber = authenticatedProcedure code: 'NOT_FOUND', message: 'Credentials not found', }) - const { display_phone_number } = (await got( - `${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}`, - { + const { display_phone_number } = await ky + .get(`${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}`, { headers: { Authorization: `Bearer ${credentials.systemUserAccessToken}`, }, - } - ).json()) as { - display_phone_number: string - } + }) + .json<{ display_phone_number: string }>() const formattedPhoneNumber = `${ display_phone_number.startsWith('+') ? '' : '+' diff --git a/apps/builder/src/features/whatsapp/getSystemTokenInfo.ts b/apps/builder/src/features/whatsapp/getSystemTokenInfo.ts index 542f0627a..18870d92b 100644 --- a/apps/builder/src/features/whatsapp/getSystemTokenInfo.ts +++ b/apps/builder/src/features/whatsapp/getSystemTokenInfo.ts @@ -1,6 +1,6 @@ import { authenticatedProcedure } from '@/helpers/server/trpc' import { z } from 'zod' -import got from 'got' +import ky from 'ky' import { TRPCError } from '@trpc/server' import { WhatsAppCredentials } from '@typebot.io/schemas/features/whatsapp' import prisma from '@typebot.io/lib/prisma' @@ -28,21 +28,23 @@ export const getSystemTokenInfo = authenticatedProcedure }) const { data: { expires_at, scopes, app_id, application }, - } = (await got( - `${env.WHATSAPP_CLOUD_API_URL}/v17.0/debug_token?input_token=${credentials.systemUserAccessToken}`, - { - headers: { - Authorization: `Bearer ${credentials.systemUserAccessToken}`, - }, - } - ).json()) as { - data: { - app_id: string - application: string - expires_at: number - scopes: string[] - } - } + } = await ky + .get( + `${env.WHATSAPP_CLOUD_API_URL}/v17.0/debug_token?input_token=${credentials.systemUserAccessToken}`, + { + headers: { + Authorization: `Bearer ${credentials.systemUserAccessToken}`, + }, + } + ) + .json<{ + data: { + app_id: string + application: string + expires_at: number + scopes: string[] + } + }>() return { appId: app_id, diff --git a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts index cf19ed2eb..7256932de 100644 --- a/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts +++ b/apps/builder/src/features/whatsapp/startWhatsAppPreview.ts @@ -3,7 +3,7 @@ import { z } from 'zod' import { TRPCError } from '@trpc/server' import { startSession } from '@typebot.io/bot-engine/startSession' import { env } from '@typebot.io/env' -import { HTTPError } from 'got' +import { HTTPError } from 'ky' import prisma from '@typebot.io/lib/prisma' import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase' import { restartSession } from '@typebot.io/bot-engine/queries/restartSession' @@ -169,7 +169,7 @@ export const startWhatsAppPreview = authenticatedProcedure }, }) } catch (err) { - if (err instanceof HTTPError) console.log(err.response.body) + if (err instanceof HTTPError) console.log(await err.response.text()) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Request to Meta to send preview message failed', diff --git a/apps/builder/src/pages/api/auth/[...nextauth].ts b/apps/builder/src/pages/api/auth/[...nextauth].ts index 0b467891a..178455d67 100644 --- a/apps/builder/src/pages/api/auth/[...nextauth].ts +++ b/apps/builder/src/pages/api/auth/[...nextauth].ts @@ -16,7 +16,7 @@ import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitat import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest' import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis/nodejs' -import got from 'got' +import ky from 'ky' import { env } from '@typebot.io/env' import * as Sentry from '@sentry/nextjs' import { getIp } from '@typebot.io/lib/getIp' @@ -164,10 +164,12 @@ export const getAuthOptions = ({ if (!account) return false const isNewUser = !('createdAt' in user && isDefined(user.createdAt)) if (isNewUser && user.email) { - const { body } = await got.get( - 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf' - ) - const disposableEmailDomains = body.split('\n') + const data = await ky + .get( + 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf' + ) + .text() + const disposableEmailDomains = data.split('\n') if (disposableEmailDomains.includes(user.email.split('@')[1])) return false } diff --git a/apps/builder/src/pages/api/customDomains/[domain].ts b/apps/builder/src/pages/api/customDomains/[domain].ts index faa5c353a..428eb6a79 100644 --- a/apps/builder/src/pages/api/customDomains/[domain].ts +++ b/apps/builder/src/pages/api/customDomains/[domain].ts @@ -5,7 +5,7 @@ import { methodNotAllowed, notAuthenticated, } from '@typebot.io/lib/api' -import { got } from 'got' +import ky from 'ky' import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' import { env } from '@typebot.io/env' @@ -31,9 +31,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } const deleteDomainOnVercel = (name: string) => - got.delete({ - url: `https://api.vercel.com/v8/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`, - headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` }, - }) + ky.delete( + `https://api.vercel.com/v8/projects/${env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME}/domains/${name}?teamId=${env.VERCEL_TEAM_ID}`, + { + headers: { Authorization: `Bearer ${env.VERCEL_TOKEN}` }, + } + ) export default handler diff --git a/apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testWebhook.ts b/apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testWebhook.ts new file mode 100644 index 000000000..d1bf4b11e --- /dev/null +++ b/apps/builder/src/pages/api/typebots/[typebotId]/blocks/[blockId]/testWebhook.ts @@ -0,0 +1,114 @@ +import { + Typebot, + Variable, + HttpRequest, + Block, + AnswerInSessionState, +} from '@typebot.io/schemas' +import { NextApiRequest, NextApiResponse } from 'next' +import { byId } from '@typebot.io/lib' +import { isWebhookBlock } from '@typebot.io/schemas/helpers' +import { methodNotAllowed, notFound } from '@typebot.io/lib/api' +import prisma from '@typebot.io/lib/prisma' +import { getBlockById } from '@typebot.io/schemas/helpers' +import { + executeWebhook, + parseWebhookAttributes, +} from '@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock' +import { fetchLinkedChildTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedChildTypebots' +import { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult' +import { saveLog } from '@typebot.io/bot-engine/logs/saveLog' +import { getAuthenticatedUser } from '@/features/auth/helpers/getAuthenticatedUser' + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method === 'POST') { + const user = await getAuthenticatedUser(req, res) + const typebotId = req.query.typebotId as string + const blockId = req.query.blockId as string + const resultId = req.query.resultId as string | undefined + const { variables } = ( + typeof req.body === 'string' ? JSON.parse(req.body) : req.body + ) as { + variables: Variable[] + } + const typebot = (await prisma.typebot.findUnique({ + where: { id: typebotId }, + include: { webhooks: true }, + })) as unknown as (Typebot & { webhooks: HttpRequest[] }) | null + if (!typebot) return notFound(res) + const block = typebot.groups + .flatMap((g) => g.blocks) + .find(byId(blockId)) + if (!block || !isWebhookBlock(block)) + return notFound(res, 'Webhook block not found') + const webhookId = 'webhookId' in block ? block.webhookId : undefined + const webhook = + block.options?.webhook ?? + typebot.webhooks.find((w) => { + if ('id' in w) return w.id === webhookId + return false + }) + if (!webhook) + return res + .status(404) + .send({ statusCode: 404, data: { message: `Couldn't find webhook` } }) + const { group } = getBlockById(blockId, typebot.groups) + const linkedTypebots = await fetchLinkedChildTypebots({ + isPreview: !('typebotId' in typebot), + typebots: [typebot], + userId: user?.id, + })([]) + + const answers = arrayify( + await parseSampleResult(typebot, linkedTypebots)(group.id, variables) + ) + + const parsedWebhook = await parseWebhookAttributes({ + webhook, + isCustomBody: block.options?.isCustomBody, + typebot: { + ...typebot, + variables: typebot.variables.map((v) => { + const matchingVariable = variables.find(byId(v.id)) + if (!matchingVariable) return v + return { ...v, value: matchingVariable.value } + }), + }, + answers, + }) + + if (!parsedWebhook) + return res.status(500).send({ + statusCode: 500, + data: { message: `Couldn't parse webhook attributes` }, + }) + + const { response, logs } = await executeWebhook(parsedWebhook, { + timeout: block.options?.timeout, + }) + + if (resultId) + await Promise.all( + logs?.map((log) => + saveLog({ + message: log.description, + details: log.details, + status: log.status as 'error' | 'success' | 'info', + resultId, + }) + ) ?? [] + ) + + return res.status(200).send(response) + } + return methodNotAllowed(res) +} + +const arrayify = ( + obj: Record +): AnswerInSessionState[] => + Object.entries(obj) + .map(([key, value]) => ({ key, value: value?.toString() })) + .filter((a) => a.value) as AnswerInSessionState[] + +export default handler diff --git a/apps/docs/contribute/the-forge/action.mdx b/apps/docs/contribute/the-forge/action.mdx index 529f5f0fd..6a336cef5 100644 --- a/apps/docs/contribute/the-forge/action.mdx +++ b/apps/docs/contribute/the-forge/action.mdx @@ -9,7 +9,7 @@ Here is the `sendMessage` action of the Telegram block: ```ts import { createAction, option } from '@typebot.io/forge' import { auth } from '../auth' -import { got } from 'got' +import ky from 'ky' export const sendMessage = createAction({ auth, @@ -27,14 +27,14 @@ export const sendMessage = createAction({ run: { server: async ({ credentials: { token }, options: { chatId, text } }) => { try { - await got.post(`https://api.telegram.org/bot${token}/sendMessage`, { + await ky.post(`https://api.telegram.org/bot${token}/sendMessage`, { json: { chat_id: chatId, text, }, }) } catch (error) { - console.log('ERROR', error.response.body) + console.log('ERROR', await error.response.text()) } }, }, diff --git a/apps/docs/contribute/the-forge/run.mdx b/apps/docs/contribute/the-forge/run.mdx index c0772611f..fc0802692 100644 --- a/apps/docs/contribute/the-forge/run.mdx +++ b/apps/docs/contribute/the-forge/run.mdx @@ -13,8 +13,6 @@ An action can do one of the following things: The most common action is to execute a function on the server. This is done by simply declaring that function in the action block. -If you need to use an external package that is not compatible with web browser environment, you will have to dynamically import it in the server function. You can find an example of this in the [Anthropic's Create Chat Message action](https://github.com/baptisteArno/typebot.io/blob/main/packages/forge/blocks/anthropic/actions/createChatMessage.tsx) - Example: ```ts diff --git a/apps/viewer/next.config.mjs b/apps/viewer/next.config.mjs index 73c07994b..c9a76527c 100644 --- a/apps/viewer/next.config.mjs +++ b/apps/viewer/next.config.mjs @@ -17,7 +17,7 @@ const injectViewerUrlIfVercelPreview = (val) => { ) return process.env.NEXT_PUBLIC_VIEWER_URL = `https://${process.env.VERCEL_BRANCH_URL}` - if (process.env.NEXT_PUBLIC_CHAT_API_URL.includes('{{pr_id}}')) + if (process.env.NEXT_PUBLIC_CHAT_API_URL?.includes('{{pr_id}}')) process.env.NEXT_PUBLIC_CHAT_API_URL = process.env.NEXT_PUBLIC_CHAT_API_URL.replace( '{{pr_id}}', @@ -56,12 +56,14 @@ const nextConfig = { if (nextRuntime === 'edge') { config.resolve.alias['minio'] = false config.resolve.alias['got'] = false + config.resolve.alias['qrcode'] = false return config } // These packages are imports from the integrations definition files that can be ignored for the client. config.resolve.alias['minio'] = false config.resolve.alias['got'] = false config.resolve.alias['openai'] = false + config.resolve.alias['qrcode'] = false return config }, async redirects() { diff --git a/apps/viewer/package.json b/apps/viewer/package.json index d0845777b..fc04ff46a 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -26,6 +26,7 @@ "cors": "2.8.5", "google-spreadsheet": "4.1.1", "got": "12.6.0", + "ky": "1.2.3", "next": "14.1.0", "nextjs-cors": "2.1.2", "nodemailer": "6.9.8", diff --git a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts index 490371fc4..27d099c77 100644 --- a/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts +++ b/apps/viewer/src/pages/api/typebots/[typebotId]/blocks/[blockId]/executeWebhook.ts @@ -1,45 +1,35 @@ import { - PublicTypebot, ResultValues, Typebot, Variable, HttpRequest, - HttpResponse, Block, + PublicTypebot, + AnswerInSessionState, } from '@typebot.io/schemas' import { NextApiRequest, NextApiResponse } from 'next' -import got, { Method, Headers, HTTPError } from 'got' -import { byId, isEmpty, isNotDefined, omit } from '@typebot.io/lib' +import { byId } from '@typebot.io/lib' import { isWebhookBlock } from '@typebot.io/schemas/helpers' -import { parseAnswers } from '@typebot.io/results/parseAnswers' import { initMiddleware, methodNotAllowed, notFound } from '@typebot.io/lib/api' -import { stringify } from 'qs' import Cors from 'cors' import prisma from '@typebot.io/lib/prisma' -import { fetchLinkedTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots' -import { getPreviouslyLinkedTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots' -import { parseVariables } from '@typebot.io/variables/parseVariables' -import { saveErrorLog } from '@typebot.io/bot-engine/logs/saveErrorLog' -import { saveSuccessLog } from '@typebot.io/bot-engine/logs/saveSuccessLog' -import { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult' -import { - HttpMethod, - defaultTimeout, - defaultWebhookAttributes, - maxTimeout, -} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' import { getBlockById } from '@typebot.io/schemas/helpers' import { - convertKeyValueTableToObject, - longReqTimeoutWhitelist, + executeWebhook, + parseWebhookAttributes, } from '@typebot.io/bot-engine/blocks/integrations/webhook/executeWebhookBlock' -import { env } from '@typebot.io/env' +import { fetchLinkedParentTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedParentTypebots' +import { fetchLinkedChildTypebots } from '@typebot.io/bot-engine/blocks/logic/typebotLink/fetchLinkedChildTypebots' +import { parseSampleResult } from '@typebot.io/bot-engine/blocks/integrations/webhook/parseSampleResult' +import { saveLog } from '@typebot.io/bot-engine/logs/saveLog' +import { authenticateUser } from '@/helpers/authenticateUser' const cors = initMiddleware(Cors()) const handler = async (req: NextApiRequest, res: NextApiResponse) => { await cors(req, res) if (req.method === 'POST') { + const user = await authenticateUser(req) const typebotId = req.query.typebotId as string const blockId = req.query.blockId as string const resultId = req.query.resultId as string | undefined @@ -72,232 +62,81 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { .status(404) .send({ statusCode: 404, data: { message: `Couldn't find webhook` } }) const { group } = getBlockById(blockId, typebot.groups) - const result = await executeWebhook(typebot)({ - webhook, - variables, - groupId: group.id, - resultValues, - resultId, + const linkedTypebotsParents = (await fetchLinkedParentTypebots({ + isPreview: !('typebotId' in typebot), parentTypebotIds, + userId: user?.id, + })) as (Typebot | PublicTypebot)[] + const linkedTypebotsChildren = await fetchLinkedChildTypebots({ + isPreview: !('typebotId' in typebot), + typebots: [typebot], + userId: user?.id, + })([]) + + const linkedTypebots = [...linkedTypebotsParents, ...linkedTypebotsChildren] + + const answers = resultValues + ? resultValues.answers.map((answer) => ({ + key: + (answer.variableId + ? typebot.variables.find( + (variable) => variable.id === answer.variableId + )?.name + : typebot.groups.find((group) => + group.blocks.find((block) => block.id === answer.blockId) + )?.title) ?? '', + value: answer.content, + })) + : arrayify( + await parseSampleResult(typebot, linkedTypebots)(group.id, variables) + ) + + const parsedWebhook = await parseWebhookAttributes({ + webhook, isCustomBody: block.options?.isCustomBody, + typebot: { + ...typebot, + variables: typebot.variables.map((v) => { + const matchingVariable = variables.find(byId(v.id)) + if (!matchingVariable) return v + return { ...v, value: matchingVariable.value } + }), + }, + answers, + }) + + if (!parsedWebhook) + return res.status(500).send({ + statusCode: 500, + data: { message: `Couldn't parse webhook attributes` }, + }) + + const { response, logs } = await executeWebhook(parsedWebhook, { timeout: block.options?.timeout, }) - return res.status(200).send(result) + + if (resultId) + await Promise.all( + logs?.map((log) => + saveLog({ + message: log.description, + details: log.details, + status: log.status as 'error' | 'success' | 'info', + resultId, + }) + ) ?? [] + ) + + return res.status(200).send(response) } return methodNotAllowed(res) } -const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body) - -export const executeWebhook = - (typebot: Typebot) => - async ({ - webhook, - variables, - groupId, - resultValues, - resultId, - parentTypebotIds = [], - isCustomBody, - timeout, - }: { - webhook: HttpRequest - variables: Variable[] - groupId: string - resultValues?: ResultValues - resultId?: string - parentTypebotIds: string[] - isCustomBody?: boolean - timeout?: number - }): Promise => { - if (!webhook.url) - return { - statusCode: 400, - data: { message: `Webhook doesn't have url or method` }, - } - const basicAuth: { username?: string; password?: string } = {} - const basicAuthHeaderIdx = - webhook.headers?.findIndex( - (h) => - h.key?.toLowerCase() === 'authorization' && - h.value?.toLowerCase()?.includes('basic') - ) ?? -1 - const isUsernamePasswordBasicAuth = - basicAuthHeaderIdx !== -1 && - webhook.headers?.[basicAuthHeaderIdx].value?.includes(':') - if (isUsernamePasswordBasicAuth) { - const [username, password] = - webhook.headers?.[basicAuthHeaderIdx].value?.slice(6).split(':') ?? [] - basicAuth.username = username - basicAuth.password = password - webhook.headers?.splice(basicAuthHeaderIdx, 1) - } - const headers = convertKeyValueTableToObject(webhook.headers, variables) as - | Headers - | undefined - const queryParams = stringify( - convertKeyValueTableToObject(webhook.queryParams, variables) - ) - const contentType = headers ? headers['Content-Type'] : undefined - const linkedTypebotsParents = (await fetchLinkedTypebots({ - isPreview: !('typebotId' in typebot), - typebotIds: parentTypebotIds, - })) as (Typebot | PublicTypebot)[] - const linkedTypebotsChildren = await getPreviouslyLinkedTypebots({ - isPreview: !('typebotId' in typebot), - typebots: [typebot], - })([]) - const bodyContent = await getBodyContent(typebot, [ - ...linkedTypebotsParents, - ...linkedTypebotsChildren, - ])({ - body: webhook.body, - isCustomBody, - resultValues, - groupId, - variables, - }) - const { data: body, isJson } = - bodyContent && webhook.method !== HttpMethod.GET - ? safeJsonParse( - parseVariables(variables, { - isInsideJson: !checkIfBodyIsAVariable(bodyContent), - })(bodyContent) - ) - : { data: undefined, isJson: false } - - const url = parseVariables(variables)( - webhook.url + (queryParams !== '' ? `?${queryParams}` : '') - ) - - const isLongRequest = longReqTimeoutWhitelist.some((whiteListedUrl) => - url?.includes(whiteListedUrl) - ) - - const request = { - url, - method: (webhook.method ?? defaultWebhookAttributes.method) as Method, - headers: headers ?? {}, - ...basicAuth, - json: - !contentType?.includes('x-www-form-urlencoded') && body && isJson - ? body - : undefined, - form: - contentType?.includes('x-www-form-urlencoded') && body - ? body - : undefined, - body: body && !isJson ? body : undefined, - timeout: { - response: isNotDefined(env.CHAT_API_TIMEOUT) - ? undefined - : timeout && timeout !== defaultTimeout - ? Math.min(timeout, maxTimeout) * 1000 - : isLongRequest - ? maxTimeout * 1000 - : defaultTimeout * 1000, - }, - } - try { - const response = await got(request.url, omit(request, 'url')) - await saveSuccessLog({ - resultId, - message: 'Webhook successfuly executed.', - details: { - statusCode: response.statusCode, - request, - response: safeJsonParse(response.body).data, - }, - }) - return { - statusCode: response.statusCode, - data: safeJsonParse(response.body).data, - } - } catch (error) { - if (error instanceof HTTPError) { - const response = { - statusCode: error.response.statusCode, - data: safeJsonParse(error.response.body as string).data, - } - await saveErrorLog({ - resultId, - message: 'Webhook returned an error', - details: { - request, - response, - }, - }) - return response - } - const response = { - statusCode: 500, - data: { message: `Error from Typebot server: ${error}` }, - } - console.error(error) - await saveErrorLog({ - resultId, - message: 'Webhook failed to execute', - details: { - request, - response, - }, - }) - return response - } - } - -const getBodyContent = - ( - typebot: Pick, - linkedTypebots: (Typebot | PublicTypebot)[] - ) => - async ({ - body, - resultValues, - groupId, - variables, - isCustomBody, - }: { - body?: string | null - resultValues?: ResultValues - groupId: string - variables: Variable[] - isCustomBody?: boolean - }): Promise => { - return body === '{{state}}' || isEmpty(body) || isCustomBody !== true - ? JSON.stringify( - resultValues - ? parseAnswers({ - answers: resultValues.answers.map((answer) => ({ - key: - (answer.variableId - ? typebot.variables.find( - (variable) => variable.id === answer.variableId - )?.name - : typebot.groups.find((group) => - group.blocks.find( - (block) => block.id === answer.blockId - ) - )?.title) ?? '', - value: answer.content, - })), - variables: resultValues.variables, - }) - : await parseSampleResult(typebot, linkedTypebots)( - groupId, - variables - ) - ) - : body ?? undefined - } - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const safeJsonParse = (json: string): { data: any; isJson: boolean } => { - try { - return { data: JSON.parse(json), isJson: true } - } catch (err) { - return { data: json, isJson: false } - } -} +const arrayify = ( + obj: Record +): AnswerInSessionState[] => + Object.entries(obj) + .map(([key, value]) => ({ key, value: value?.toString() })) + .filter((a) => a.value) as AnswerInSessionState[] export default handler diff --git a/packages/bot-engine/apiHandlers/continueChat.ts b/packages/bot-engine/apiHandlers/continueChat.ts index aa6ab8689..86aa41ba2 100644 --- a/packages/bot-engine/apiHandlers/continueChat.ts +++ b/packages/bot-engine/apiHandlers/continueChat.ts @@ -13,7 +13,6 @@ type Props = { sessionId: string } export const continueChat = async ({ origin, sessionId, message }: Props) => { - console.log('test') const session = await getSession(sessionId) if (!session) { diff --git a/packages/bot-engine/blocks/integrations/legacy/openai/executeChatCompletionOpenAIRequest.ts b/packages/bot-engine/blocks/integrations/legacy/openai/executeChatCompletionOpenAIRequest.ts index 72c67920d..9a0735710 100644 --- a/packages/bot-engine/blocks/integrations/legacy/openai/executeChatCompletionOpenAIRequest.ts +++ b/packages/bot-engine/blocks/integrations/legacy/openai/executeChatCompletionOpenAIRequest.ts @@ -1,7 +1,7 @@ import { isNotEmpty } from '@typebot.io/lib/utils' import { ContinueChatResponse } from '@typebot.io/schemas' import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai' -import { HTTPError } from 'got' +import { HTTPError } from 'ky' import { ClientOptions, OpenAI } from 'openai' type Props = Pick< @@ -55,9 +55,9 @@ export const executeChatCompletionOpenAIRequest = async ({ } catch (error) { if (error instanceof HTTPError) { if ( - (error.response.statusCode === 503 || - error.response.statusCode === 500 || - error.response.statusCode === 403) && + (error.response.status === 503 || + error.response.status === 500 || + error.response.status === 403) && !isRetrying ) { console.log('OpenAI API error - 503, retrying in 3 seconds') @@ -73,7 +73,7 @@ export const executeChatCompletionOpenAIRequest = async ({ isRetrying: true, }) } - if (error.response.statusCode === 400) { + if (error.response.status === 400) { const log = { status: 'info', description: @@ -93,8 +93,8 @@ export const executeChatCompletionOpenAIRequest = async ({ } logs.push({ status: 'error', - description: `OpenAI API error - ${error.response.statusCode}`, - details: error.response.body, + description: `OpenAI API error - ${error.response.status}`, + details: await error.response.text(), }) return { logs } } diff --git a/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts b/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts index 2e8ab6961..d7f0a7d1a 100644 --- a/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts +++ b/packages/bot-engine/blocks/integrations/webhook/executeWebhookBlock.ts @@ -11,10 +11,11 @@ import { ChatLog, ExecutableHttpRequest, AnswerInSessionState, + TypebotInSession, } from '@typebot.io/schemas' import { stringify } from 'qs' import { isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib' -import got, { Method, HTTPError, OptionsInit } from 'got' +import ky, { HTTPError, Options } from 'ky' import { resumeWebhookExecution } from './resumeWebhookExecution' import { ExecuteIntegrationResponse } from '../../../types' import { parseVariables } from '@typebot.io/variables/parseVariables' @@ -60,9 +61,11 @@ export const executeWebhookBlock = async ( })) as HttpRequest | null) : null) if (!webhook) return { outgoingEdgeId: block.outgoingEdgeId } - const parsedWebhook = await parseWebhookAttributes(state)({ + const parsedWebhook = await parseWebhookAttributes({ webhook, isCustomBody: block.options?.isCustomBody, + typebot: state.typebotsQueue[0].typebot, + answers: state.typebotsQueue[0].answers, }) if (!parsedWebhook) { logs.push({ @@ -104,69 +107,69 @@ export const executeWebhookBlock = async ( const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body) -const parseWebhookAttributes = - (state: SessionState) => - async ({ - webhook, - isCustomBody, - }: { - webhook: HttpRequest - isCustomBody?: boolean - }): Promise => { - if (!webhook.url) return - const { typebot } = state.typebotsQueue[0] - const basicAuth: { username?: string; password?: string } = {} - const basicAuthHeaderIdx = webhook.headers?.findIndex( - (h) => - h.key?.toLowerCase() === 'authorization' && - h.value?.toLowerCase()?.includes('basic') - ) - const isUsernamePasswordBasicAuth = - basicAuthHeaderIdx !== -1 && - isDefined(basicAuthHeaderIdx) && - webhook.headers?.at(basicAuthHeaderIdx)?.value?.includes(':') - if (isUsernamePasswordBasicAuth) { - const [username, password] = - webhook.headers?.at(basicAuthHeaderIdx)?.value?.slice(6).split(':') ?? - [] - basicAuth.username = username - basicAuth.password = password - webhook.headers?.splice(basicAuthHeaderIdx, 1) - } - const headers = convertKeyValueTableToObject( - webhook.headers, - typebot.variables - ) as ExecutableHttpRequest['headers'] | undefined - const queryParams = stringify( - convertKeyValueTableToObject(webhook.queryParams, typebot.variables) - ) - const bodyContent = await getBodyContent({ - body: webhook.body, - answers: state.typebotsQueue[0].answers, - variables: typebot.variables, - isCustomBody, - }) - const method = webhook.method ?? defaultWebhookAttributes.method - const { data: body, isJson } = - bodyContent && method !== HttpMethod.GET - ? safeJsonParse( - parseVariables(typebot.variables, { - isInsideJson: !checkIfBodyIsAVariable(bodyContent), - })(bodyContent) - ) - : { data: undefined, isJson: false } - - return { - url: parseVariables(typebot.variables)( - webhook.url + (queryParams !== '' ? `?${queryParams}` : '') - ), - basicAuth, - method, - headers, - body, - isJson, - } +export const parseWebhookAttributes = async ({ + webhook, + isCustomBody, + typebot, + answers, +}: { + webhook: HttpRequest + isCustomBody?: boolean + typebot: TypebotInSession + answers: AnswerInSessionState[] +}): Promise => { + if (!webhook.url) return + const basicAuth: { username?: string; password?: string } = {} + const basicAuthHeaderIdx = webhook.headers?.findIndex( + (h) => + h.key?.toLowerCase() === 'authorization' && + h.value?.toLowerCase()?.includes('basic') + ) + const isUsernamePasswordBasicAuth = + basicAuthHeaderIdx !== -1 && + isDefined(basicAuthHeaderIdx) && + webhook.headers?.at(basicAuthHeaderIdx)?.value?.includes(':') + if (isUsernamePasswordBasicAuth) { + const [username, password] = + webhook.headers?.at(basicAuthHeaderIdx)?.value?.slice(6).split(':') ?? [] + basicAuth.username = username + basicAuth.password = password + webhook.headers?.splice(basicAuthHeaderIdx, 1) } + const headers = convertKeyValueTableToObject( + webhook.headers, + typebot.variables + ) as ExecutableHttpRequest['headers'] | undefined + const queryParams = stringify( + convertKeyValueTableToObject(webhook.queryParams, typebot.variables) + ) + const bodyContent = await getBodyContent({ + body: webhook.body, + answers, + variables: typebot.variables, + isCustomBody, + }) + const method = webhook.method ?? defaultWebhookAttributes.method + const { data: body, isJson } = + bodyContent && method !== HttpMethod.GET + ? safeJsonParse( + parseVariables(typebot.variables, { + isInsideJson: !checkIfBodyIsAVariable(bodyContent), + })(bodyContent) + ) + : { data: undefined, isJson: false } + + return { + url: parseVariables(typebot.variables)( + webhook.url + (queryParams !== '' ? `?${queryParams}` : '') + ), + basicAuth, + method, + headers, + body, + isJson, + } +} export const executeWebhook = async ( webhook: ParsedWebhook, @@ -177,7 +180,8 @@ export const executeWebhook = async ( startTimeShouldBeUpdated?: boolean }> => { const logs: ChatLog[] = [] - const { headers, url, method, basicAuth, body, isJson } = webhook + + const { headers, url, method, basicAuth, isJson } = webhook const contentType = headers ? headers['Content-Type'] : undefined const isLongRequest = params.disableRequestTimeout @@ -186,59 +190,60 @@ export const executeWebhook = async ( url?.includes(whiteListedUrl) ) + const isFormData = contentType?.includes('x-www-form-urlencoded') + + let body = webhook.body + + if (isFormData && isJson) body = parseFormDataBody(body as object) + const request = { url, - method: method as Method, + method, headers: headers ?? {}, ...(basicAuth ?? {}), - json: - !contentType?.includes('x-www-form-urlencoded') && body && isJson - ? body - : undefined, - form: - contentType?.includes('x-www-form-urlencoded') && body ? body : undefined, - body: body && !isJson ? (body as string) : undefined, - timeout: { - response: isNotDefined(env.CHAT_API_TIMEOUT) - ? undefined - : params.timeout && params.timeout !== defaultTimeout - ? Math.min(params.timeout, maxTimeout) * 1000 - : isLongRequest - ? maxTimeout * 1000 - : defaultTimeout * 1000, - }, - } satisfies OptionsInit + json: !isFormData && body && isJson ? body : undefined, + body: (isFormData && body ? body : undefined) as any, + timeout: isNotDefined(env.CHAT_API_TIMEOUT) + ? undefined + : params.timeout && params.timeout !== defaultTimeout + ? Math.min(params.timeout, maxTimeout) * 1000 + : isLongRequest + ? maxTimeout * 1000 + : defaultTimeout * 1000, + } satisfies Options & { url: string; body: any } try { - const response = await got(request.url, omit(request, 'url')) + const response = await ky(request.url, omit(request, 'url')) + const body = await response.text() logs.push({ status: 'success', description: webhookSuccessDescription, details: { - statusCode: response.statusCode, - response: safeJsonParse(response.body).data, + statusCode: response.status, + response: safeJsonParse(body).data, request, }, }) return { response: { - statusCode: response.statusCode, - data: safeJsonParse(response.body).data, + statusCode: response.status, + data: safeJsonParse(body).data, }, logs, startTimeShouldBeUpdated: true, } } catch (error) { if (error instanceof HTTPError) { + const responseBody = await error.response.text() const response = { - statusCode: error.response.statusCode, - data: safeJsonParse(error.response.body as string).data, + statusCode: error.response.status, + data: safeJsonParse(responseBody).data, } logs.push({ status: 'error', description: webhookErrorDescription, details: { - statusCode: error.response.statusCode, + statusCode: error.response.status, request, response, }, @@ -257,7 +262,7 @@ export const executeWebhook = async ( } logs.push({ status: 'error', - description: `Webhook request timed out. (${request.timeout.response}ms)`, + description: `Webhook request timed out. (${request.timeout}ms)`, details: { response, request, @@ -320,10 +325,18 @@ export const convertKeyValueTableToObject = ( } // eslint-disable-next-line @typescript-eslint/no-explicit-any -const safeJsonParse = (json: string): { data: any; isJson: boolean } => { +const safeJsonParse = (json: unknown): { data: any; isJson: boolean } => { try { - return { data: JSON.parse(json), isJson: true } + return { data: JSON.parse(json as string), isJson: true } } catch (err) { return { data: json, isJson: false } } } + +const parseFormDataBody = (body: object) => { + const searchParams = new URLSearchParams() + Object.entries(body as object).forEach(([key, value]) => { + searchParams.set(key, value) + }) + return searchParams +} diff --git a/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts b/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts index 9c28c2959..01f70e6b9 100644 --- a/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts +++ b/packages/bot-engine/blocks/integrations/zemanticAi/executeZemanticAiBlock.ts @@ -4,7 +4,7 @@ import { ZemanticAiCredentials, ZemanticAiResponse, } from '@typebot.io/schemas/features/blocks/integrations/zemanticAi' -import got from 'got' +import ky from 'ky' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' import { byId, isDefined, isEmpty } from '@typebot.io/lib' import { ExecuteIntegrationResponse } from '../../../types' @@ -51,7 +51,7 @@ export const executeZemanticAiBlock = async ( }) try { - const res: ZemanticAiResponse = await got + const res: ZemanticAiResponse = await ky .post(URL, { headers: { Authorization: `Bearer ${apiKey}`, diff --git a/packages/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots.ts b/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedChildTypebots.ts similarity index 89% rename from packages/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots.ts rename to packages/bot-engine/blocks/logic/typebotLink/fetchLinkedChildTypebots.ts index b7524d15f..d7b6d579a 100644 --- a/packages/bot-engine/blocks/logic/typebotLink/getPreviouslyLinkedTypebots.ts +++ b/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedChildTypebots.ts @@ -11,12 +11,12 @@ import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/consta type Props = { typebots: Pick[] - user?: User + userId: string | undefined isPreview?: boolean } -export const getPreviouslyLinkedTypebots = - ({ typebots, user, isPreview }: Props) => +export const fetchLinkedChildTypebots = + ({ typebots, userId, isPreview }: Props) => async ( capturedLinkedBots: (Typebot | PublicTypebot)[] ): Promise<(Typebot | PublicTypebot)[]> => { @@ -40,13 +40,13 @@ export const getPreviouslyLinkedTypebots = .filter(isDefined) if (linkedTypebotIds.length === 0) return capturedLinkedBots const linkedTypebots = (await fetchLinkedTypebots({ - user, + userId, typebotIds: linkedTypebotIds, isPreview, })) as (Typebot | PublicTypebot)[] - return getPreviouslyLinkedTypebots({ + return fetchLinkedChildTypebots({ typebots: linkedTypebots, - user, + userId, isPreview, })([...capturedLinkedBots, ...linkedTypebots]) } diff --git a/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedParentTypebots.ts b/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedParentTypebots.ts new file mode 100644 index 000000000..1f6590757 --- /dev/null +++ b/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedParentTypebots.ts @@ -0,0 +1,20 @@ +import { fetchLinkedTypebots } from './fetchLinkedTypebots' + +type Props = { + parentTypebotIds: string[] + userId: string | undefined + isPreview?: boolean +} + +export const fetchLinkedParentTypebots = ({ + parentTypebotIds, + isPreview, + userId, +}: Props) => + parentTypebotIds.length > 0 + ? fetchLinkedTypebots({ + typebotIds: parentTypebotIds, + isPreview, + userId, + }) + : [] diff --git a/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots.ts b/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots.ts index d41b788b1..a2a2f1e30 100644 --- a/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots.ts +++ b/packages/bot-engine/blocks/logic/typebotLink/fetchLinkedTypebots.ts @@ -1,20 +1,19 @@ import prisma from '@typebot.io/lib/prisma' -import { User } from '@typebot.io/prisma' type Props = { isPreview?: boolean typebotIds: string[] - user?: User + userId: string | undefined } export const fetchLinkedTypebots = async ({ - user, + userId, isPreview, typebotIds, }: Props) => { - if (!user || !isPreview) + if (!userId || !isPreview) return prisma.publicTypebot.findMany({ - where: { id: { in: typebotIds } }, + where: { typebotId: { in: typebotIds } }, }) const linkedTypebots = await prisma.typebot.findMany({ where: { id: { in: typebotIds } }, @@ -39,7 +38,7 @@ export const fetchLinkedTypebots = async ({ return linkedTypebots.filter( (typebot) => typebot.collaborators.some( - (collaborator) => collaborator.userId === user.id - ) || typebot.workspace.members.some((member) => member.userId === user.id) + (collaborator) => collaborator.userId === userId + ) || typebot.workspace.members.some((member) => member.userId === userId) ) } diff --git a/packages/bot-engine/executeGroup.ts b/packages/bot-engine/executeGroup.ts index ac9eb1392..c5beeb59f 100644 --- a/packages/bot-engine/executeGroup.ts +++ b/packages/bot-engine/executeGroup.ts @@ -111,7 +111,6 @@ export const executeGroup = async ( logs, visitedEdges, } - console.log('yes') const executionResponse = ( isLogicBlock(block) ? await executeLogic(newSessionState)(block) diff --git a/packages/bot-engine/executeIntegration.ts b/packages/bot-engine/executeIntegration.ts index ba28836bf..0820e4995 100644 --- a/packages/bot-engine/executeIntegration.ts +++ b/packages/bot-engine/executeIntegration.ts @@ -16,7 +16,6 @@ import { env } from '@typebot.io/env' export const executeIntegration = (state: SessionState) => async (block: IntegrationBlock): Promise => { - console.log('HI') switch (block.type) { case IntegrationBlockType.GOOGLE_SHEETS: return { diff --git a/packages/bot-engine/forge/executeForgedBlock.ts b/packages/bot-engine/forge/executeForgedBlock.ts index 5ee8d13ac..1306a1a3b 100644 --- a/packages/bot-engine/forge/executeForgedBlock.ts +++ b/packages/bot-engine/forge/executeForgedBlock.ts @@ -27,7 +27,6 @@ export const executeForgedBlock = async ( const blockDef = forgedBlocks[block.type] if (!blockDef) return { outgoingEdgeId: block.outgoingEdgeId } const action = blockDef.actions.find((a) => a.name === block.options.action) - console.log('test', action) const noCredentialsError = { status: 'error', description: 'Credentials not provided for integration', diff --git a/packages/bot-engine/package.json b/packages/bot-engine/package.json index 3e0a17b2e..ddc968e18 100644 --- a/packages/bot-engine/package.json +++ b/packages/bot-engine/package.json @@ -14,6 +14,7 @@ "@typebot.io/env": "workspace:*", "@typebot.io/lib": "workspace:*", "@typebot.io/prisma": "workspace:*", + "@typebot.io/results": "workspace:*", "@typebot.io/schemas": "workspace:*", "@typebot.io/tsconfig": "workspace:*", "@typebot.io/variables": "workspace:*", @@ -24,19 +25,18 @@ "date-fns-tz": "2.0.0", "google-auth-library": "8.9.0", "google-spreadsheet": "4.1.1", - "got": "12.6.0", - "ky": "^1.1.3", + "ky": "1.2.3", "libphonenumber-js": "1.10.37", "node-html-parser": "6.1.5", "nodemailer": "6.9.8", "openai": "4.28.4", "qs": "6.11.2", - "stripe": "12.13.0", - "@typebot.io/results": "workspace:*" + "stripe": "12.13.0" }, "devDependencies": { "@typebot.io/forge": "workspace:*", "@typebot.io/forge-repository": "workspace:*", + "@types/node": "^20.12.3", "@types/nodemailer": "6.4.14", "@types/qs": "6.9.7" } diff --git a/packages/bot-engine/whatsapp/downloadMedia.ts b/packages/bot-engine/whatsapp/downloadMedia.ts index aead0cf5c..d89abceec 100644 --- a/packages/bot-engine/whatsapp/downloadMedia.ts +++ b/packages/bot-engine/whatsapp/downloadMedia.ts @@ -1,5 +1,5 @@ import { env } from '@typebot.io/env' -import got from 'got' +import ky from 'ky' type Props = { mediaId: string @@ -10,21 +10,24 @@ export const downloadMedia = async ({ mediaId, systemUserAccessToken, }: Props): Promise<{ file: Buffer; mimeType: string }> => { - const { body } = await got.get({ - url: `${env.WHATSAPP_CLOUD_API_URL}/v17.0/${mediaId}`, - headers: { - Authorization: `Bearer ${systemUserAccessToken}`, - }, - }) - - const parsedBody = JSON.parse(body) as { url: string; mime_type: string } - - return { - file: await got(parsedBody.url, { + const { url, mime_type } = await ky + .get(`${env.WHATSAPP_CLOUD_API_URL}/v17.0/${mediaId}`, { headers: { Authorization: `Bearer ${systemUserAccessToken}`, }, - }).buffer(), - mimeType: parsedBody.mime_type, + }) + .json<{ url: string; mime_type: string }>() + + return { + file: Buffer.from( + await ky + .get(url, { + headers: { + Authorization: `Bearer ${systemUserAccessToken}`, + }, + }) + .arrayBuffer() + ), + mimeType: mime_type, } } diff --git a/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts index c268b7dc3..a7c9ffe98 100644 --- a/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts +++ b/packages/bot-engine/whatsapp/sendChatReplyToWhatsApp.ts @@ -10,7 +10,7 @@ import { import { convertMessageToWhatsAppMessage } from './convertMessageToWhatsAppMessage' import { sendWhatsAppMessage } from './sendWhatsAppMessage' import * as Sentry from '@sentry/nextjs' -import { HTTPError } from 'got' +import { HTTPError } from 'ky' import { convertInputToWhatsAppMessages } from './convertInputToWhatsAppMessage' import { isNotDefined } from '@typebot.io/lib/utils' import { computeTypingDuration } from '../computeTypingDuration' @@ -141,7 +141,7 @@ export const sendChatReplyToWhatsApp = async ({ Sentry.captureException(err, { extra: { message } }) console.log('Failed to send message:', JSON.stringify(message, null, 2)) if (err instanceof HTTPError) - console.log('HTTPError', err.response.statusCode, err.response.body) + console.log('HTTPError', err.response.status, await err.response.text()) } } @@ -172,7 +172,11 @@ export const sendChatReplyToWhatsApp = async ({ Sentry.captureException(err, { extra: { message } }) console.log('Failed to send message:', JSON.stringify(message, null, 2)) if (err instanceof HTTPError) - console.log('HTTPError', err.response.statusCode, err.response.body) + console.log( + 'HTTPError', + err.response.status, + await err.response.text() + ) } } } @@ -253,7 +257,11 @@ const executeClientSideAction = Sentry.captureException(err, { extra: { message } }) console.log('Failed to send message:', JSON.stringify(message, null, 2)) if (err instanceof HTTPError) - console.log('HTTPError', err.response.statusCode, err.response.body) + console.log( + 'HTTPError', + err.response.status, + await err.response.text() + ) } } } diff --git a/packages/bot-engine/whatsapp/sendWhatsAppMessage.ts b/packages/bot-engine/whatsapp/sendWhatsAppMessage.ts index 7bc9e3c9e..9bafbeb40 100644 --- a/packages/bot-engine/whatsapp/sendWhatsAppMessage.ts +++ b/packages/bot-engine/whatsapp/sendWhatsAppMessage.ts @@ -3,7 +3,7 @@ import { WhatsAppSendingMessage, } from '@typebot.io/schemas/features/whatsapp' import { env } from '@typebot.io/env' -import got from 'got' +import ky from 'ky' type Props = { to: string @@ -16,7 +16,7 @@ export const sendWhatsAppMessage = async ({ message, credentials, }: Props) => - got.post( + ky.post( `${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}/messages`, { headers: { diff --git a/packages/forge/blocks/anthropic/actions/createChatMessage.tsx b/packages/forge/blocks/anthropic/actions/createChatMessage.tsx index ed0e994e2..26c366605 100644 --- a/packages/forge/blocks/anthropic/actions/createChatMessage.tsx +++ b/packages/forge/blocks/anthropic/actions/createChatMessage.tsx @@ -1,5 +1,6 @@ import { createAction, option } from '@typebot.io/forge' import { auth } from '../auth' +import { Anthropic } from '@anthropic-ai/sdk' import { AnthropicStream } from 'ai' import { anthropicModels, defaultAnthropicOptions } from '../constants' import { parseChatMessages } from '../helpers/parseChatMessages' @@ -103,8 +104,6 @@ export const createChatMessage = createAction({ responseMapping?.map((res) => res.variableId).filter(isDefined) ?? [], run: { server: async ({ credentials: { apiKey }, options, variables, logs }) => { - const { Anthropic } = await import('@anthropic-ai/sdk') - const client = new Anthropic({ apiKey: apiKey, }) @@ -150,8 +149,6 @@ export const createChatMessage = createAction({ (res) => res.item === 'Message Content' || !res.item )?.variableId, run: async ({ credentials: { apiKey }, options, variables }) => { - const { Anthropic } = await import('@anthropic-ai/sdk') - const client = new Anthropic({ apiKey: apiKey, }) diff --git a/packages/forge/blocks/chatNode/actions/sendMessage.ts b/packages/forge/blocks/chatNode/actions/sendMessage.ts index dabb9f1fc..951909026 100644 --- a/packages/forge/blocks/chatNode/actions/sendMessage.ts +++ b/packages/forge/blocks/chatNode/actions/sendMessage.ts @@ -1,6 +1,6 @@ import { createAction, option } from '@typebot.io/forge' import { isDefined, isEmpty } from '@typebot.io/lib' -import { HTTPError, got } from 'got' +import ky, { HTTPError } from 'ky' import { apiBaseUrl } from '../constants' import { auth } from '../auth' import { ChatNodeResponse } from '../types' @@ -40,7 +40,7 @@ export const sendMessage = createAction({ logs, }) => { try { - const res: ChatNodeResponse = await got + const res: ChatNodeResponse = await ky .post(apiBaseUrl + botId, { headers: { Authorization: `Bearer ${apiKey}`, @@ -66,7 +66,7 @@ export const sendMessage = createAction({ return logs.add({ status: 'error', description: error.message, - details: error.response.body, + details: await error.response.text(), }) console.error(error) } diff --git a/packages/forge/blocks/chatNode/package.json b/packages/forge/blocks/chatNode/package.json index d1eb64088..2e697d07d 100644 --- a/packages/forge/blocks/chatNode/package.json +++ b/packages/forge/blocks/chatNode/package.json @@ -11,6 +11,6 @@ "@typebot.io/tsconfig": "workspace:*", "@types/react": "18.2.15", "typescript": "5.3.2", - "got": "12.6.0" + "ky": "1.2.3" } } diff --git a/packages/forge/blocks/difyAi/actions/createChatMessage.ts b/packages/forge/blocks/difyAi/actions/createChatMessage.ts index a508da57d..338ac0864 100644 --- a/packages/forge/blocks/difyAi/actions/createChatMessage.ts +++ b/packages/forge/blocks/difyAi/actions/createChatMessage.ts @@ -1,9 +1,9 @@ import { createAction, option } from '@typebot.io/forge' import { isDefined, isEmpty, isNotEmpty } from '@typebot.io/lib' -import { HTTPError, got } from 'got' import { auth } from '../auth' import { defaultBaseUrl } from '../constants' import { Chunk } from '../types' +import ky from 'ky' export const createChatMessage = createAction({ auth, @@ -44,13 +44,15 @@ export const createChatMessage = createAction({ logs, }) => { try { - const stream = got.post( + const response = await ky( (apiEndpoint ?? defaultBaseUrl) + '/v1/chat-messages', { + method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', }, - json: { + body: JSON.stringify({ inputs: inputs?.reduce((acc, { key, value }) => { if (isEmpty(key) || isEmpty(value)) return acc @@ -64,56 +66,70 @@ export const createChatMessage = createAction({ conversation_id, user, files: [], - }, - isStream: true, + }), } ) + const reader = response.body?.getReader() + + if (!reader) + return logs.add({ + status: 'error', + description: 'Failed to read response stream', + }) + const { answer, conversationId, totalTokens } = await new Promise<{ answer: string conversationId: string | undefined totalTokens: number | undefined - }>((resolve, reject) => { + }>(async (resolve, reject) => { let jsonChunk = '' let answer = '' let conversationId: string | undefined let totalTokens: number | undefined - stream.on('data', (chunk) => { - const lines = chunk.toString().split('\n') as string[] - lines - .filter((line) => line.length > 0 && line !== '\n') - .forEach((line) => { - jsonChunk += line - if (jsonChunk.startsWith('event: ')) { + try { + while (true) { + const { value, done } = await reader.read() + if (done) { + resolve({ answer, conversationId, totalTokens }) + return + } + + const chunk = new TextDecoder().decode(value) + + const lines = chunk.toString().split('\n') as string[] + lines + .filter((line) => line.length > 0 && line !== '\n') + .forEach((line) => { + jsonChunk += line + if (jsonChunk.startsWith('event: ')) { + jsonChunk = '' + return + } + if ( + !jsonChunk.startsWith('data: ') || + !jsonChunk.endsWith('}') + ) + return + + const data = JSON.parse(jsonChunk.slice(6)) as Chunk jsonChunk = '' - return - } - if (!jsonChunk.startsWith('data: ') || !jsonChunk.endsWith('}')) - return - - const data = JSON.parse(jsonChunk.slice(6)) as Chunk - jsonChunk = '' - if ( - data.event === 'message' || - data.event === 'agent_message' - ) { - answer += data.answer - } - if (data.event === 'message_end') { - totalTokens = data.metadata.usage.total_tokens - conversationId = data.conversation_id - } - }) - }) - - stream.on('end', () => { - resolve({ answer, conversationId, totalTokens }) - }) - - stream.on('error', (error) => { - reject(error) - }) + if ( + data.event === 'message' || + data.event === 'agent_message' + ) { + answer += data.answer + } + if (data.event === 'message_end') { + totalTokens = data.metadata.usage.total_tokens + conversationId = data.conversation_id + } + }) + } + } catch (e) { + reject(e) + } }) responseMapping?.forEach((mapping) => { @@ -130,12 +146,10 @@ export const createChatMessage = createAction({ variables.set(mapping.variableId, totalTokens) }) } catch (error) { - if (error instanceof HTTPError) - return logs.add({ - status: 'error', - description: error.message, - details: error.response.body, - }) + logs.add({ + status: 'error', + description: 'Failed to create chat message', + }) console.error(error) } }, diff --git a/packages/forge/blocks/difyAi/package.json b/packages/forge/blocks/difyAi/package.json index dcebf57c8..2174ee540 100644 --- a/packages/forge/blocks/difyAi/package.json +++ b/packages/forge/blocks/difyAi/package.json @@ -10,7 +10,7 @@ "@typebot.io/lib": "workspace:*", "@typebot.io/tsconfig": "workspace:*", "@types/react": "18.2.15", - "got": "12.6.0", + "ky": "1.2.3", "typescript": "5.3.2" } } diff --git a/packages/forge/blocks/elevenlabs/actions/convertTextToSpeech.ts b/packages/forge/blocks/elevenlabs/actions/convertTextToSpeech.ts index 0bed7b5ff..6b428418e 100644 --- a/packages/forge/blocks/elevenlabs/actions/convertTextToSpeech.ts +++ b/packages/forge/blocks/elevenlabs/actions/convertTextToSpeech.ts @@ -2,7 +2,7 @@ import { createAction, option } from '@typebot.io/forge' import { auth } from '../auth' import { baseUrl } from '../constants' import { ModelsResponse, VoicesResponse } from '../type' -import got, { HTTPError } from 'got' +import got, { HTTPError } from 'ky' import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket' import { createId } from '@typebot.io/lib/createId' @@ -93,10 +93,10 @@ export const convertTextToSpeech = createAction({ text: options.text, }, }) - .buffer() + .arrayBuffer() const url = await uploadFileToBucket({ - file: response, + file: Buffer.from(response), key: `tmp/elevenlabs/audio/${createId() + createId()}.mp3`, mimeType: 'audio/mpeg', }) @@ -107,7 +107,7 @@ export const convertTextToSpeech = createAction({ return logs.add({ status: 'error', description: err.message, - details: err.response.body, + details: await err.response.text(), }) } } diff --git a/packages/forge/blocks/elevenlabs/package.json b/packages/forge/blocks/elevenlabs/package.json index 4c366350f..f97412dc1 100644 --- a/packages/forge/blocks/elevenlabs/package.json +++ b/packages/forge/blocks/elevenlabs/package.json @@ -13,7 +13,7 @@ "typescript": "5.3.2" }, "dependencies": { - "got": "12.6.0", + "ky": "1.2.3", "@typebot.io/lib": "workspace:*" } } diff --git a/packages/forge/blocks/mistral/actions/createChatCompletion.ts b/packages/forge/blocks/mistral/actions/createChatCompletion.ts index f1a59d444..6e2c3c364 100644 --- a/packages/forge/blocks/mistral/actions/createChatCompletion.ts +++ b/packages/forge/blocks/mistral/actions/createChatCompletion.ts @@ -3,6 +3,8 @@ import { isDefined } from '@typebot.io/lib' import { auth } from '../auth' import { parseMessages } from '../helpers/parseMessages' import { OpenAIStream } from 'ai' +// @ts-ignore +import MistralClient from '../helpers/client' const nativeMessageContentSchema = { content: option.string.layout({ @@ -95,15 +97,17 @@ export const createChatCompletion = createAction({ id: 'fetchModels', dependencies: [], fetch: async ({ credentials }) => { - const MistralClient = (await import('@mistralai/mistralai')).default const client = new MistralClient(credentials.apiKey) - const listModelsResponse = await client.listModels() + const listModelsResponse: any = await client.listModels() return ( listModelsResponse.data - .sort((a, b) => b.created - a.created) - .map((model) => model.id) ?? [] + .sort( + (a: { created: number }, b: { created: number }) => + b.created - a.created + ) + .map((model: { id: any }) => model.id) ?? [] ) }, }, @@ -111,10 +115,9 @@ export const createChatCompletion = createAction({ run: { server: async ({ credentials: { apiKey }, options, variables, logs }) => { if (!options.model) return logs.add('No model selected') - const MistralClient = (await import('@mistralai/mistralai')).default const client = new MistralClient(apiKey) - const response = await client.chat({ + const response: any = await client.chat({ model: options.model, messages: parseMessages({ options, variables }), }) @@ -132,15 +135,13 @@ export const createChatCompletion = createAction({ )?.variableId, run: async ({ credentials: { apiKey }, options, variables }) => { if (!options.model) return - const MistralClient = (await import('@mistralai/mistralai')).default const client = new MistralClient(apiKey) - const response = client.chatStream({ + const response: any = client.chatStream({ model: options.model, messages: parseMessages({ options, variables }), }) - // @ts-ignore https://github.com/vercel/ai/issues/936 return OpenAIStream(response) }, }, diff --git a/packages/forge/blocks/mistral/helpers/client.js b/packages/forge/blocks/mistral/helpers/client.js new file mode 100644 index 000000000..48bce84aa --- /dev/null +++ b/packages/forge/blocks/mistral/helpers/client.js @@ -0,0 +1,341 @@ +// Taken from https://github.com/mistralai/client-js/blob/main/src/client.js +// Lib seems not actively maintained, and we need this patch: https://github.com/mistralai/client-js/pull/42 + +let isNode = false + +let fetch + +const VERSION = '0.0.3' +const RETRY_STATUS_CODES = [429, 500, 502, 503, 504] +const ENDPOINT = 'https://api.mistral.ai' + +/** + * Initialize fetch + * @return {Promise} + */ +async function initializeFetch() { + if (typeof globalThis.fetch === 'undefined') + throw new Error('No fetch implementation found') + if (typeof window === 'undefined') { + isNode = true + } + fetch = globalThis.fetch +} + +initializeFetch() + +/** + * MistralAPIError + * @return {MistralAPIError} + * @extends {Error} + */ +class MistralAPIError extends Error { + /** + * A simple error class for Mistral API errors + * @param {*} message + */ + constructor(message) { + super(message) + this.name = 'MistralAPIError' + } +} + +/** + * MistralClient + * @return {MistralClient} + */ +class MistralClient { + /** + * A simple and lightweight client for the Mistral API + * @param {*} apiKey can be set as an environment variable MISTRAL_API_KEY, + * or provided in this parameter + * @param {*} endpoint defaults to https://api.mistral.ai + * @param {*} maxRetries defaults to 5 + * @param {*} timeout defaults to 120 seconds + */ + constructor( + apiKey = process.env.MISTRAL_API_KEY, + endpoint = ENDPOINT, + maxRetries = 5, + timeout = 120 + ) { + this.endpoint = endpoint + this.apiKey = apiKey + + this.maxRetries = maxRetries + this.timeout = timeout + + if (this.endpoint.indexOf('inference.azure.com')) { + this.modelDefault = 'mistral' + } + } + + /** + * + * @param {*} method + * @param {*} path + * @param {*} request + * @return {Promise<*>} + */ + _request = async function (method, path, request) { + const url = `${this.endpoint}/${path}` + const options = { + method: method, + headers: { + 'User-Agent': `mistral-client-js/${VERSION}`, + Accept: request?.stream ? 'text/event-stream' : 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }, + body: method !== 'get' ? JSON.stringify(request) : null, + timeout: this.timeout * 1000, + } + + for (let attempts = 0; attempts < this.maxRetries; attempts++) { + try { + const response = await fetch(url, options) + + if (response.ok) { + if (request?.stream) { + if (isNode) { + return response.body + } else { + const reader = response.body.getReader() + // Chrome does not support async iterators yet, so polyfill it + const asyncIterator = async function* () { + try { + while (true) { + // Read from the stream + const { done, value } = await reader.read() + // Exit if we're done + if (done) return + // Else yield the chunk + yield value + } + } finally { + reader.releaseLock() + } + } + + return asyncIterator() + } + } + return await response.json() + } else if (RETRY_STATUS_CODES.includes(response.status)) { + console.debug( + `Retrying request on response status: ${response.status}`, + `Response: ${await response.text()}`, + `Attempt: ${attempts + 1}` + ) + // eslint-disable-next-line max-len + await new Promise((resolve) => + setTimeout(resolve, Math.pow(2, attempts + 1) * 500) + ) + } else { + throw new MistralAPIError( + `HTTP error! status: ${response.status} ` + + `Response: \n${await response.text()}` + ) + } + } catch (error) { + console.error(`Request failed: ${error.message}`) + if (error.name === 'MistralAPIError') { + throw error + } + if (attempts === this.maxRetries - 1) throw error + // eslint-disable-next-line max-len + await new Promise((resolve) => + setTimeout(resolve, Math.pow(2, attempts + 1) * 500) + ) + } + } + throw new Error('Max retries reached') + } + + /** + * Creates a chat completion request + * @param {*} model + * @param {*} messages + * @param {*} tools + * @param {*} temperature + * @param {*} maxTokens + * @param {*} topP + * @param {*} randomSeed + * @param {*} stream + * @param {*} safeMode deprecated use safePrompt instead + * @param {*} safePrompt + * @param {*} toolChoice + * @param {*} responseFormat + * @return {Promise} + */ + _makeChatCompletionRequest = function ( + model, + messages, + tools, + temperature, + maxTokens, + topP, + randomSeed, + stream, + safeMode, + safePrompt, + toolChoice, + responseFormat + ) { + // if modelDefault and model are undefined, throw an error + if (!model && !this.modelDefault) { + throw new MistralAPIError('You must provide a model name') + } + return { + model: model ?? this.modelDefault, + messages: messages, + tools: tools ?? undefined, + temperature: temperature ?? undefined, + max_tokens: maxTokens ?? undefined, + top_p: topP ?? undefined, + random_seed: randomSeed ?? undefined, + stream: stream ?? undefined, + safe_prompt: (safeMode || safePrompt) ?? undefined, + tool_choice: toolChoice ?? undefined, + response_format: responseFormat ?? undefined, + } + } + + /** + * Returns a list of the available models + * @return {Promise} + */ + listModels = async function () { + const response = await this._request('get', 'v1/models') + return response + } + + /** + * A chat endpoint without streaming + * @param {*} model the name of the model to chat with, e.g. mistral-tiny + * @param {*} messages an array of messages to chat with, e.g. + * [{role: 'user', content: 'What is the best French cheese?'}] + * @param {*} tools a list of tools to use. + * @param {*} temperature the temperature to use for sampling, e.g. 0.5 + * @param {*} maxTokens the maximum number of tokens to generate, e.g. 100 + * @param {*} topP the cumulative probability of tokens to generate, e.g. 0.9 + * @param {*} randomSeed the random seed to use for sampling, e.g. 42 + * @param {*} safeMode deprecated use safePrompt instead + * @param {*} safePrompt whether to use safe mode, e.g. true + * @param {*} toolChoice the tool to use, e.g. 'auto' + * @param {*} responseFormat the format of the response, e.g. 'json_format' + * @return {Promise} + */ + chat = async function ({ + model, + messages, + tools, + temperature, + maxTokens, + topP, + randomSeed, + safeMode, + safePrompt, + toolChoice, + responseFormat, + }) { + const request = this._makeChatCompletionRequest( + model, + messages, + tools, + temperature, + maxTokens, + topP, + randomSeed, + false, + safeMode, + safePrompt, + toolChoice, + responseFormat + ) + const response = await this._request('post', 'v1/chat/completions', request) + return response + } + + /** + * A chat endpoint that streams responses. + * @param {*} model the name of the model to chat with, e.g. mistral-tiny + * @param {*} messages an array of messages to chat with, e.g. + * [{role: 'user', content: 'What is the best French cheese?'}] + * @param {*} tools a list of tools to use. + * @param {*} temperature the temperature to use for sampling, e.g. 0.5 + * @param {*} maxTokens the maximum number of tokens to generate, e.g. 100 + * @param {*} topP the cumulative probability of tokens to generate, e.g. 0.9 + * @param {*} randomSeed the random seed to use for sampling, e.g. 42 + * @param {*} safeMode deprecated use safePrompt instead + * @param {*} safePrompt whether to use safe mode, e.g. true + * @param {*} toolChoice the tool to use, e.g. 'auto' + * @param {*} responseFormat the format of the response, e.g. 'json_format' + * @return {Promise} + */ + chatStream = async function* ({ + model, + messages, + tools, + temperature, + maxTokens, + topP, + randomSeed, + safeMode, + safePrompt, + toolChoice, + responseFormat, + }) { + const request = this._makeChatCompletionRequest( + model, + messages, + tools, + temperature, + maxTokens, + topP, + randomSeed, + true, + safeMode, + safePrompt, + toolChoice, + responseFormat + ) + const response = await this._request('post', 'v1/chat/completions', request) + + let buffer = '' + const decoder = new TextDecoder() + for await (const chunk of response) { + buffer += decoder.decode(chunk, { stream: true }) + let firstNewline + while ((firstNewline = buffer.indexOf('\n')) !== -1) { + const chunkLine = buffer.substring(0, firstNewline) + buffer = buffer.substring(firstNewline + 1) + if (chunkLine.startsWith('data:')) { + const json = chunkLine.substring(6).trim() + if (json !== '[DONE]') { + yield JSON.parse(json) + } + } + } + } + } + + /** + * An embeddings endpoint that returns embeddings for a single, + * or batch of inputs + * @param {*} model The embedding model to use, e.g. mistral-embed + * @param {*} input The input to embed, + * e.g. ['What is the best French cheese?'] + * @return {Promise} + */ + embeddings = async function ({ model, input }) { + const request = { + model: model, + input: input, + } + const response = await this._request('post', 'v1/embeddings', request) + return response + } +} + +export default MistralClient diff --git a/packages/forge/blocks/mistral/package.json b/packages/forge/blocks/mistral/package.json index a10740354..945555861 100644 --- a/packages/forge/blocks/mistral/package.json +++ b/packages/forge/blocks/mistral/package.json @@ -9,11 +9,11 @@ "@typebot.io/forge": "workspace:*", "@typebot.io/lib": "workspace:*", "@typebot.io/tsconfig": "workspace:*", + "@types/node": "^20.12.4", "@types/react": "18.2.15", "typescript": "5.3.2" }, "dependencies": { - "@mistralai/mistralai": "0.1.3", "ai": "3.0.12" } } diff --git a/packages/forge/blocks/mistral/tsconfig.json b/packages/forge/blocks/mistral/tsconfig.json index 950c0c115..fb4fe45e8 100644 --- a/packages/forge/blocks/mistral/tsconfig.json +++ b/packages/forge/blocks/mistral/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@typebot.io/tsconfig/base.json", - "include": ["**/*.ts", "**/*.tsx"], + "include": ["**/*.ts", "**/*.tsx", "helpers/client.ts"], "exclude": ["node_modules"], "compilerOptions": { "lib": ["ESNext", "DOM"], diff --git a/packages/forge/blocks/openRouter/actions/createChatCompletion.tsx b/packages/forge/blocks/openRouter/actions/createChatCompletion.tsx index 5b3a7c909..fdaabbf9b 100644 --- a/packages/forge/blocks/openRouter/actions/createChatCompletion.tsx +++ b/packages/forge/blocks/openRouter/actions/createChatCompletion.tsx @@ -6,7 +6,7 @@ import { getChatCompletionStreamVarId } from '@typebot.io/openai-block/shared/ge import { runChatCompletion } from '@typebot.io/openai-block/shared/runChatCompletion' import { runChatCompletionStream } from '@typebot.io/openai-block/shared/runChatCompletionStream' import { defaultOpenRouterOptions } from '../constants' -import { got } from 'got' +import ky from 'ky' import { ModelsResponse } from '../types' export const createChatCompletion = createAction({ @@ -42,7 +42,7 @@ export const createChatCompletion = createAction({ id: 'fetchModels', dependencies: [], fetch: async () => { - const response = await got + const response = await ky .get(defaultOpenRouterOptions.baseUrl + '/models') .json() diff --git a/packages/forge/blocks/openRouter/package.json b/packages/forge/blocks/openRouter/package.json index 4703d6190..6b1067b4e 100644 --- a/packages/forge/blocks/openRouter/package.json +++ b/packages/forge/blocks/openRouter/package.json @@ -12,6 +12,6 @@ "typescript": "5.3.2", "@typebot.io/lib": "workspace:*", "@typebot.io/openai-block": "workspace:*", - "got": "12.6.0" + "ky": "1.2.3" } } diff --git a/packages/forge/blocks/qrcode/actions/generateQrCodeImage.ts b/packages/forge/blocks/qrcode/actions/generateQrCodeImage.ts index e5396cfc5..f1506fb92 100644 --- a/packages/forge/blocks/qrcode/actions/generateQrCodeImage.ts +++ b/packages/forge/blocks/qrcode/actions/generateQrCodeImage.ts @@ -1,4 +1,5 @@ import { createAction, option } from '@typebot.io/forge' +import { toBuffer as generateQrCodeBuffer } from 'qrcode' import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket' import { createId } from '@typebot.io/lib/createId' @@ -28,8 +29,6 @@ export const generateQrCode = createAction({ 'QR code image URL is not specified. Please select a variable to save the generated QR code image.' ) - const generateQrCodeBuffer = (await import('qrcode')).toBuffer - const url = await uploadFileToBucket({ file: await generateQrCodeBuffer(options.data), key: `tmp/qrcodes/${createId() + createId()}.png`, diff --git a/packages/forge/blocks/zemanticAi/actions/searchDocuments.ts b/packages/forge/blocks/zemanticAi/actions/searchDocuments.ts index baf2397d2..89e0e4b91 100644 --- a/packages/forge/blocks/zemanticAi/actions/searchDocuments.ts +++ b/packages/forge/blocks/zemanticAi/actions/searchDocuments.ts @@ -1,7 +1,7 @@ import { createAction, option } from '@typebot.io/forge' import { isDefined } from '@typebot.io/lib' import { ZemanticAiResponse } from '../types' -import { got } from 'got' +import ky from 'ky' import { apiBaseUrl } from '../constants' import { auth } from '../auth' import { baseOptions } from '../baseOptions' @@ -63,7 +63,7 @@ export const searchDocuments = createAction({ }, variables, }) => { - const res: ZemanticAiResponse = await got + const res = await ky .post(apiBaseUrl, { headers: { Authorization: `Bearer ${apiKey}`, @@ -79,7 +79,7 @@ export const searchDocuments = createAction({ }, }, }) - .json() + .json() responseMapping?.forEach((mapping) => { if (!mapping.variableId || !mapping.item) return diff --git a/packages/forge/blocks/zemanticAi/auth.ts b/packages/forge/blocks/zemanticAi/auth.ts index 88fb438d5..71298e6f8 100644 --- a/packages/forge/blocks/zemanticAi/auth.ts +++ b/packages/forge/blocks/zemanticAi/auth.ts @@ -8,6 +8,7 @@ export const auth = { label: 'API key', isRequired: true, placeholder: 'ze...', + inputType: 'password', helperText: 'You can generate an API key [here](https://zemantic.ai/dashboard/settings).', isDebounceDisabled: true, diff --git a/packages/forge/blocks/zemanticAi/index.ts b/packages/forge/blocks/zemanticAi/index.ts index 20c501d54..d7d930bbd 100644 --- a/packages/forge/blocks/zemanticAi/index.ts +++ b/packages/forge/blocks/zemanticAi/index.ts @@ -1,6 +1,6 @@ import { createBlock } from '@typebot.io/forge' import { ZemanticAiLogo } from './logo' -import { got } from 'got' +import ky from 'ky' import { searchDocuments } from './actions/searchDocuments' import { auth } from './auth' import { baseOptions } from './baseOptions' @@ -19,7 +19,7 @@ export const zemanticAiBlock = createBlock({ fetch: async ({ credentials: { apiKey } }) => { const url = 'https://api.zemantic.ai/v1/projects' - const response = await got + const response = await ky .get(url, { headers: { Authorization: `Bearer ${apiKey}`, diff --git a/packages/forge/blocks/zemanticAi/package.json b/packages/forge/blocks/zemanticAi/package.json index b8ca467cc..1db95535e 100644 --- a/packages/forge/blocks/zemanticAi/package.json +++ b/packages/forge/blocks/zemanticAi/package.json @@ -11,6 +11,6 @@ "@types/react": "18.2.15", "typescript": "5.3.2", "@typebot.io/lib": "workspace:*", - "got": "12.6.0" + "ky": "1.2.3" } } diff --git a/packages/lib/package.json b/packages/lib/package.json index 458d1602d..52f188f90 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -38,7 +38,7 @@ "@udecode/plate-paragraph": "30.5.3", "escape-html": "1.0.3", "google-auth-library": "8.9.0", - "got": "12.6.0", + "ky": "1.2.3", "minio": "7.1.3", "posthog-node": "3.1.1", "remark-parse": "11.0.0", diff --git a/packages/scripts/insertUsersInBrevoList.ts b/packages/scripts/insertUsersInBrevoList.ts index dee871a23..e7241d96d 100644 --- a/packages/scripts/insertUsersInBrevoList.ts +++ b/packages/scripts/insertUsersInBrevoList.ts @@ -1,8 +1,7 @@ import { PrismaClient } from '@typebot.io/prisma' import { promptAndSetEnvironment } from './utils' -import got, { HTTPError } from 'got' +import ky, { HTTPError } from 'ky' import { confirm, text, isCancel } from '@clack/prompts' -import { writeFileSync } from 'fs' const insertUsersInBrevoList = async () => { await promptAndSetEnvironment('production') @@ -44,7 +43,7 @@ const insertUsersInBrevoList = async () => { } try { - await got.post('https://api.brevo.com/v3/contacts/import', { + await ky.post('https://api.brevo.com/v3/contacts/import', { headers: { 'api-key': process.env.BREVO_API_KEY, }, @@ -58,7 +57,7 @@ const insertUsersInBrevoList = async () => { }) } catch (err) { if (err instanceof HTTPError) { - console.log(err.response.body) + console.log(await err.response.text()) return } console.log(err) diff --git a/packages/scripts/package.json b/packages/scripts/package.json index e37c98a72..7cc2d54cc 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -42,7 +42,7 @@ "@types/papaparse": "5.3.7", "@types/prompts": "2.4.4", "deep-object-diff": "1.1.9", - "got": "12.6.0", + "ky": "1.2.3", "prompts": "2.4.2", "stripe": "12.13.0", "tsx": "3.12.7", diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index a733fed19..b836b2757 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -8,7 +8,7 @@ "license": "ISC", "dependencies": { "@typebot.io/schemas": "workspace:*", - "got": "12.6.0", + "ky": "1.2.3", "posthog-node": "3.1.1", "@typebot.io/env": "workspace:*" }, diff --git a/packages/telemetry/trackEvents.ts b/packages/telemetry/trackEvents.ts index 5308cddc7..3e2b0d866 100644 --- a/packages/telemetry/trackEvents.ts +++ b/packages/telemetry/trackEvents.ts @@ -1,7 +1,7 @@ import { env } from '@typebot.io/env' import { TelemetryEvent } from '@typebot.io/schemas/features/telemetry' import { PostHog } from 'posthog-node' -import got from 'got' +import ky from 'ky' export const trackEvents = async (events: TelemetryEvent[]) => { if (!env.NEXT_PUBLIC_POSTHOG_KEY) return @@ -17,7 +17,7 @@ export const trackEvents = async (events: TelemetryEvent[]) => { }) if (env.USER_CREATED_WEBHOOK_URL) { try { - await got.post(env.USER_CREATED_WEBHOOK_URL, { + await ky.post(env.USER_CREATED_WEBHOOK_URL, { json: { email: event.data.email, name: event.data.name ? event.data.name.split(' ')[0] : undefined, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 807bc72ca..d12a91cd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,15 +191,15 @@ importers: google-spreadsheet: specifier: 4.1.1 version: 4.1.1(google-auth-library@8.9.0) - got: - specifier: 12.6.0 - version: 12.6.0 immer: specifier: 10.0.2 version: 10.0.2 jsonwebtoken: specifier: 9.0.1 version: 9.0.1 + ky: + specifier: 1.2.3 + version: 1.2.3 libphonenumber-js: specifier: 1.10.37 version: 1.10.37 @@ -593,6 +593,9 @@ importers: got: specifier: 12.6.0 version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 next: specifier: 14.1.0 version: 14.1.0(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) @@ -781,12 +784,9 @@ importers: google-spreadsheet: specifier: 4.1.1 version: 4.1.1(google-auth-library@8.9.0) - got: - specifier: 12.6.0 - version: 12.6.0 ky: - specifier: ^1.1.3 - version: 1.2.0 + specifier: 1.2.3 + version: 1.2.3 libphonenumber-js: specifier: 1.10.37 version: 1.10.37 @@ -812,6 +812,9 @@ importers: '@typebot.io/forge-repository': specifier: workspace:* version: link:../forge/repository + '@types/node': + specifier: ^20.12.3 + version: 20.12.3 '@types/nodemailer': specifier: 6.4.14 version: 6.4.14 @@ -1334,9 +1337,9 @@ importers: '@types/react': specifier: 18.2.15 version: 18.2.15 - got: - specifier: 12.6.0 - version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 typescript: specifier: 5.3.2 version: 5.3.2 @@ -1355,9 +1358,9 @@ importers: '@types/react': specifier: 18.2.15 version: 18.2.15 - got: - specifier: 12.6.0 - version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 typescript: specifier: 5.3.2 version: 5.3.2 @@ -1367,9 +1370,9 @@ importers: '@typebot.io/lib': specifier: workspace:* version: link:../../../lib - got: - specifier: 12.6.0 - version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 devDependencies: '@typebot.io/forge': specifier: workspace:* @@ -1386,9 +1389,6 @@ importers: packages/forge/blocks/mistral: dependencies: - '@mistralai/mistralai': - specifier: 0.1.3 - version: 0.1.3 ai: specifier: 3.0.12 version: 3.0.12(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4) @@ -1402,6 +1402,9 @@ importers: '@typebot.io/tsconfig': specifier: workspace:* version: link:../../../tsconfig + '@types/node': + specifier: ^20.12.4 + version: 20.12.4 '@types/react': specifier: 18.2.15 version: 18.2.15 @@ -1426,9 +1429,9 @@ importers: '@types/react': specifier: 18.2.15 version: 18.2.15 - got: - specifier: 12.6.0 - version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 typescript: specifier: 5.3.2 version: 5.3.2 @@ -1524,9 +1527,9 @@ importers: '@types/react': specifier: 18.2.15 version: 18.2.15 - got: - specifier: 12.6.0 - version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 typescript: specifier: 5.3.2 version: 5.3.2 @@ -1645,9 +1648,9 @@ importers: google-auth-library: specifier: 8.9.0 version: 8.9.0 - got: - specifier: 12.6.0 - version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 minio: specifier: 7.1.3 version: 7.1.3 @@ -1884,9 +1887,9 @@ importers: deep-object-diff: specifier: 1.1.9 version: 1.1.9 - got: - specifier: 12.6.0 - version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 prompts: specifier: 2.4.2 version: 2.4.2 @@ -1911,9 +1914,9 @@ importers: '@typebot.io/schemas': specifier: workspace:* version: link:../schemas - got: - specifier: 12.6.0 - version: 12.6.0 + ky: + specifier: 1.2.3 + version: 1.2.3 posthog-node: specifier: 3.1.1 version: 3.1.1 @@ -6838,7 +6841,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -6859,14 +6862,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.11.26) + jest-config: 29.7.0(@types/node@20.12.3) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -6894,7 +6897,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 jest-mock: 29.7.0 dev: true @@ -6921,7 +6924,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.11.26 + '@types/node': 20.12.3 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -6954,7 +6957,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.25 - '@types/node': 20.11.26 + '@types/node': 20.12.3 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -7042,7 +7045,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.11.26 + '@types/node': 20.12.3 '@types/yargs': 17.0.32 chalk: 4.1.2 dev: true @@ -7461,14 +7464,6 @@ packages: zod-to-json-schema: 3.22.4(zod@3.22.4) dev: true - /@mistralai/mistralai@0.1.3: - resolution: {integrity: sha512-WUHxC2xdeqX9PTXJEqdiNY54vT2ir72WSJrZTTBKRnkfhX6zIfCYA24faRlWjUB5WTpn+wfdGsTMl3ArijlXFA==} - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - dev: false - /@next/env@14.0.5-canary.46: resolution: {integrity: sha512-dvNzrArTfe3VY1VIscpb3E2e7SZ1qwFe82WGzpOVbxilT3JcsnVGYF/uq8Jj1qKWPI5C/aePNXwA97JRNAXpRQ==} dev: false @@ -7827,7 +7822,7 @@ packages: engines: {node: '>=16'} hasBin: true dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 playwright-core: 1.36.0 optionalDependencies: fsevents: 2.3.2 @@ -9453,7 +9448,7 @@ packages: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: false /@types/canvas-confetti@1.6.0: @@ -9463,13 +9458,13 @@ packages: /@types/cli-progress@3.11.5: resolution: {integrity: sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: true /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: false /@types/content-type@1.1.8: @@ -9482,7 +9477,7 @@ packages: /@types/cors@2.8.13: resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 /@types/debug@4.1.12: resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -9528,7 +9523,7 @@ packages: /@types/express-serve-static-core@4.17.43: resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 '@types/qs': 6.9.7 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -9546,7 +9541,7 @@ packages: /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: true /@types/hast@2.3.10: @@ -9602,7 +9597,7 @@ packages: /@types/jsdom@20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 '@types/tough-cookie': 4.0.5 parse5: 7.1.2 dev: true @@ -9617,7 +9612,7 @@ packages: /@types/jsonwebtoken@9.0.2: resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: true /@types/katex@0.16.7: @@ -9655,7 +9650,7 @@ packages: /@types/micro@7.3.7: resolution: {integrity: sha512-MFsX7eCj0Tg3TtphOQvANNvNtFpya+s/rYOCdV6o+DFjOQPFi2EVRbBALjbbgZTXUaJP1Q281MJiJOD40d0UxQ==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: true /@types/mime@1.3.5: @@ -9676,7 +9671,7 @@ packages: /@types/node-fetch@2.6.11: resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 form-data: 4.0.0 dev: false @@ -9687,6 +9682,18 @@ packages: resolution: {integrity: sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==} dependencies: undici-types: 5.26.5 + dev: true + + /@types/node@20.12.3: + resolution: {integrity: sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==} + dependencies: + undici-types: 5.26.5 + + /@types/node@20.12.4: + resolution: {integrity: sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==} + dependencies: + undici-types: 5.26.5 + dev: true /@types/node@20.4.2: resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==} @@ -9698,13 +9705,13 @@ packages: /@types/nodemailer@6.4.14: resolution: {integrity: sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: true /@types/nodemailer@6.4.8: resolution: {integrity: sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: true /@types/normalize-package-data@2.4.4: @@ -9717,7 +9724,7 @@ packages: /@types/papaparse@5.3.7: resolution: {integrity: sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: true /@types/parse-json@4.0.2: @@ -9735,7 +9742,7 @@ packages: /@types/prompts@2.4.4: resolution: {integrity: sha512-p5N9uoTH76lLvSAaYSZtBCdEXzpOOufsRjnhjVSrZGXikVGHX9+cc9ERtHRV4hvBKHyZb1bg4K+56Bd2TqUn4A==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 kleur: 3.0.3 dev: true @@ -9745,7 +9752,7 @@ packages: /@types/qrcode@1.5.5: resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: true /@types/qs@6.9.7: @@ -9801,7 +9808,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: false /@types/serve-static@1.15.5: @@ -9809,7 +9816,7 @@ packages: dependencies: '@types/http-errors': 2.0.4 '@types/mime': 3.0.4 - '@types/node': 20.11.26 + '@types/node': 20.12.3 dev: false /@types/stack-utils@2.0.3: @@ -9841,7 +9848,7 @@ packages: /@types/webpack@5.28.5(@swc/core@1.3.101)(esbuild@0.19.11): resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 tapable: 2.2.1 webpack: 5.90.3(@swc/core@1.3.101)(esbuild@0.19.11) transitivePeerDependencies: @@ -12457,7 +12464,7 @@ packages: chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.11.26) + jest-config: 29.7.0(@types/node@20.12.3) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -13170,7 +13177,7 @@ packages: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.13 - '@types/node': 20.11.26 + '@types/node': 20.12.3 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -15928,7 +15935,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -15966,7 +15973,7 @@ packages: create-jest: 29.7.0 exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.11.26) + jest-config: 29.7.0(@types/node@20.12.3) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -15977,7 +15984,7 @@ packages: - ts-node dev: true - /jest-config@29.7.0(@types/node@20.11.26): + /jest-config@29.7.0(@types/node@20.12.3): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -15992,7 +15999,7 @@ packages: '@babel/core': 7.22.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 babel-jest: 29.7.0(@babel/core@7.22.9) chalk: 4.1.2 ci-info: 3.9.0 @@ -16058,7 +16065,7 @@ packages: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 20.11.26 + '@types/node': 20.12.3 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -16075,7 +16082,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -16091,7 +16098,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.11.26 + '@types/node': 20.12.3 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -16142,7 +16149,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 jest-util: 29.7.0 dev: true @@ -16197,7 +16204,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -16228,7 +16235,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -16280,7 +16287,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -16305,7 +16312,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.26 + '@types/node': 20.12.3 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -16317,7 +16324,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 merge-stream: 2.0.0 supports-color: 8.1.1 dev: false @@ -16326,7 +16333,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -16658,10 +16665,9 @@ packages: engines: {node: '>=18'} dev: false - /ky@1.2.0: - resolution: {integrity: sha512-dnPW+T78MuJ9tLAiF/apJV7bP7RRRCARXQxsCmsWiKLXqGtMBOgDVOFRYzCAfNe/OrRyFyor5ESgvvC+QWEqOA==} + /ky@1.2.3: + resolution: {integrity: sha512-2IM3VssHfG2zYz2FsHRUqIp8chhLc9uxDMcK2THxgFfv8pQhnMfN8L0ul+iW4RdBl5AglF8ooPIflRm3yNH0IA==} engines: {node: '>=18'} - dev: false /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} @@ -18540,7 +18546,7 @@ packages: engines: {node: '>=14'} dependencies: '@types/express': 4.17.21 - '@types/node': 20.11.26 + '@types/node': 20.12.3 accepts: 1.3.8 content-disposition: 0.5.4 depd: 1.1.2 @@ -21400,7 +21406,7 @@ packages: resolution: {integrity: sha512-mn7CxL71FCRWkQp33jcJ7+xfRF7HGzPYZlq2c87U+6kxL1qd7f/N3S1g1E5uaSWe83V5v3jN/IiWqg9y8+kWRw==} engines: {node: '>=12.*'} dependencies: - '@types/node': 20.11.26 + '@types/node': 20.12.3 qs: 6.11.2 /strnum@1.0.5: