2
0

Introduce a new high-performing standalone chat API (#1200)

Closes #1154

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
	- Added authentication functionality for user sessions in chat API.
- Introduced chat-related API endpoints for starting, previewing, and
continuing chat sessions, and streaming messages.
- Implemented WhatsApp API webhook handling for receiving and processing
messages.
- Added environment variable `NEXT_PUBLIC_CHAT_API_URL` for chat API URL
configuration.

- **Bug Fixes**
	- Adjusted file upload logic to correctly determine the API host.
	- Fixed message streaming URL in chat integration with OpenAI.

- **Documentation**
- Updated guides for creating blocks, local installation, self-hosting,
and deployment to use `bun` instead of `pnpm`.

- **Refactor**
	- Refactored chat API functionalities to use modular architecture.
- Simplified client log saving and session update functionalities by
using external functions.
	- Transitioned package management and workflow commands to use `bun`.

- **Chores**
- Switched to `bun` for package management in Dockerfiles and GitHub
workflows.
	- Added new Dockerfile for chat API service setup with Bun framework.
	- Updated `.prettierignore` and documentation with new commands.

- **Style**
	- No visible changes to end-users.

- **Tests**
	- No visible changes to end-users.

- **Revert**
	- No reverts in this release.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2024-03-21 10:23:23 +01:00
committed by GitHub
parent 5b9176708c
commit 2fcf83c529
51 changed files with 1446 additions and 494 deletions

View File

@ -21,6 +21,12 @@ 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}}'))
process.env.NEXT_PUBLIC_CHAT_API_URL =
process.env.NEXT_PUBLIC_CHAT_API_URL.replace(
'{{pr_id}}',
process.env.VERCEL_GIT_PULL_REQUEST_ID
)
}
injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL)

View File

@ -75,7 +75,7 @@
"next": "14.1.0",
"next-auth": "4.22.1",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.3",
"nodemailer": "6.9.8",
"nprogress": "0.2.0",
"openai": "4.28.4",
"papaparse": "5.4.1",
@ -114,7 +114,7 @@
"@types/jsonwebtoken": "9.0.2",
"@types/micro-cors": "0.1.3",
"@types/node": "20.4.2",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.14",
"@types/nprogress": "0.2.0",
"@types/papaparse": "5.3.7",
"@types/prettier": "2.7.3",

View File

@ -114,6 +114,7 @@ export const startWhatsAppPreview = authenticatedProcedure
typebotId,
startFrom,
userId: user.id,
isStreamEnabled: false,
},
initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined)

View File

@ -0,0 +1,36 @@
{
"name": "chat-api",
"version": "1.0.0",
"license": "AGPL-3.0-or-later",
"private": true,
"scripts": {
"dev": "dotenv -e ./.env -e ../../.env -- bun --hot src/index.ts",
"build": "dotenv -e ./.env -e ../../.env -- bun build --target=bun ./src/index.ts --outdir ./dist",
"start": "bun src/index.ts"
},
"dependencies": {
"@hono/prometheus": "1.0.0",
"@hono/sentry": "1.0.1",
"@hono/typebox-validator": "0.2.2",
"@sinclair/typebox": "0.32.5",
"@trpc/server": "10.40.0",
"@typebot.io/bot-engine": "workspace:*",
"@typebot.io/env": "workspace:*",
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/variables": "workspace:*",
"ai": "3.0.12",
"hono": "4.0.5",
"openai": "4.28.4",
"prom-client": "15.1.0"
},
"devDependencies": {
"dotenv-cli": "7.2.1",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"react": "18.2.0"
}
}

30
apps/chat-api/src/auth.ts Normal file
View File

@ -0,0 +1,30 @@
import { env } from '@typebot.io/env'
import { mockedUser } from '@typebot.io/lib/mockedUser'
import prisma from '@typebot.io/lib/prisma'
export const getAuthenticatedUserId = async (
authorizationHeaderValue: string | undefined
): Promise<string | undefined> => {
if (env.NEXT_PUBLIC_E2E_TEST) return mockedUser.id
const bearerToken = extractBearerToken(authorizationHeaderValue)
if (!bearerToken) return
return authenticateByToken(bearerToken)
}
const authenticateByToken = async (
token: string
): Promise<string | undefined> => {
if (typeof window !== 'undefined') return
const apiToken = await prisma.apiToken.findFirst({
where: {
token,
},
select: {
ownerId: true,
},
})
return apiToken?.ownerId
}
const extractBearerToken = (authorizationHeaderValue: string | undefined) =>
authorizationHeaderValue?.slice(7)

View File

@ -0,0 +1,30 @@
import { Hono } from 'hono'
import { webRuntime } from './runtimes/web'
import { whatsAppRuntime } from './runtimes/whatsapp'
import { prometheus } from '@hono/prometheus'
import { sentry } from '@hono/sentry'
import { env } from '@typebot.io/env'
const app = new Hono()
app.use(
'*',
sentry({
environment: env.NODE_ENV,
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-chat-api',
})
)
const { printMetrics, registerMetrics } = prometheus()
app.use('*', registerMetrics)
app.get('/metrics', printMetrics)
app.get('/ping', (c) => c.json({ status: 'ok' }, 200))
app.route('/', webRuntime)
app.route('/', whatsAppRuntime)
export default {
port: process.env.PORT ?? 3002,
fetch: app.fetch,
}

View File

@ -0,0 +1,132 @@
import { startChat } from '@typebot.io/bot-engine/apiHandlers/startChat'
import { continueChat } from '@typebot.io/bot-engine/apiHandlers/continueChat'
import { startChatPreview } from '@typebot.io/bot-engine/apiHandlers/startChatPreview'
import { getMessageStream } from '@typebot.io/bot-engine/apiHandlers/getMessageStream'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { tbValidator } from '@hono/typebox-validator'
import { Type as t } from '@sinclair/typebox'
import { getAuthenticatedUserId } from '../auth'
export const webRuntime = new Hono()
webRuntime.use('*', cors())
webRuntime.post(
'/api/v1/typebots/:publicId/startChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
isStreamEnabled: t.Optional(t.Boolean()),
resultId: t.Optional(t.String()),
isOnlyRegistering: t.Optional(t.Boolean()),
prefilledVariables: t.Optional(t.Record(t.String(), t.Unknown())),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { corsOrigin, ...response } = await startChat({
...data,
publicId: c.req.param('publicId'),
isStreamEnabled: data.isStreamEnabled ?? true,
isOnlyRegistering: data.isOnlyRegistering ?? false,
origin: c.req.header('origin'),
})
if (corsOrigin) c.res.headers.set('Access-Control-Allow-Origin', corsOrigin)
return c.json(response)
}
)
webRuntime.post(
'/api/v1/typebots/:id/preview/startChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
isStreamEnabled: t.Optional(t.Boolean()),
resultId: t.Optional(t.String()),
isOnlyRegistering: t.Optional(t.Boolean()),
prefilledVariables: t.Optional(t.Record(t.String(), t.Unknown())),
startFrom: t.Optional(
t.Union([
t.Object({
type: t.Literal('group'),
groupId: t.String(),
}),
t.Object({
type: t.Literal('event'),
eventId: t.String(),
}),
])
),
typebot: t.Optional(t.Any()),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const userId = !data.typebot
? await getAuthenticatedUserId(c.req.header('Authorization'))
: undefined
return c.json(
await startChatPreview({
...data,
typebotId: c.req.param('id'),
userId,
isStreamEnabled: data.isStreamEnabled ?? true,
isOnlyRegistering: data.isOnlyRegistering ?? false,
})
)
}
)
webRuntime.post(
'/api/v1/sessions/:sessionId/continueChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { corsOrigin, ...response } = await continueChat({
...data,
sessionId: c.req.param('sessionId'),
origin: c.req.header('origin'),
})
if (corsOrigin) c.res.headers.set('Access-Control-Allow-Origin', corsOrigin)
return c.json(response)
}
)
webRuntime.post(
'/api/v1/sessions/:sessionId/streamMessage',
tbValidator(
'json',
t.Object({
messages: t.Optional(t.Array(t.Any())),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { stream, status, message } = await getMessageStream({
sessionId: c.req.param('sessionId'),
messages: data.messages,
})
if (!stream) return c.json({ message }, (status ?? 400) as ResponseInit)
return new Response(stream)
}
)

View File

@ -0,0 +1,51 @@
import { receiveMessage } from '@typebot.io/bot-engine/apiHandlers/receiveMessage'
import { receiveMessagePreview } from '@typebot.io/bot-engine/apiHandlers/receiveMessagePreview'
import { tbValidator } from '@hono/typebox-validator'
import { Hono } from 'hono'
import { Type as t } from '@sinclair/typebox'
export const whatsAppRuntime = new Hono()
whatsAppRuntime.post(
'/api/v1/workspaces/:workspaceId/whatsapp/:credentialsId/webhook',
tbValidator(
'json',
t.Object({
object: t.String(),
entry: t.Any(),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
receiveMessage({
workspaceId: c.req.param('workspaceId'),
credentialsId: c.req.param('credentialsId'),
...data,
})
return c.json({ message: 'Webhook received' }, 200)
}
)
whatsAppRuntime.post(
'/api/v1/whatsapp/preview/webhook',
tbValidator(
'json',
t.Object({
object: t.String(),
entry: t.Any(),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
receiveMessagePreview({
...data,
})
return c.json({ message: 'Webhook received' }, 200)
}
)

View File

@ -0,0 +1,8 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"jsx": "react"
}
}

View File

@ -1772,14 +1772,16 @@
"type": "object",
"properties": {
"isStreamEnabled": {
"type": "boolean"
"type": "boolean",
"default": false
},
"message": {
"type": "string"
},
"isOnlyRegistering": {
"type": "boolean",
"description": "If set to `true`, it will only register the session and not start the bot. This is used for 3rd party chat platforms as it can require a session to be registered before sending the first message."
"description": "If set to `true`, it will only register the session and not start the bot. This is used for 3rd party chat platforms as it can require a session to be registered before sending the first message.",
"default": false
},
"typebot": {
"oneOf": [

View File

@ -2,7 +2,7 @@
"name": "landing-page",
"version": "1.0.0",
"scripts": {
"dev": "dotenv -e ./.env -e ../../.env -- next dev -p 3002",
"dev": "dotenv -e ./.env -e ../../.env -- next dev -p 3003",
"start": "dotenv -e ./.env -e ../../.env -- next start",
"build": "dotenv -e ./.env -e ../../.env -- next build",
"lint": "next lint",

View File

@ -17,6 +17,12 @@ 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}}'))
process.env.NEXT_PUBLIC_CHAT_API_URL =
process.env.NEXT_PUBLIC_CHAT_API_URL.replace(
'{{pr_id}}',
process.env.VERCEL_GIT_PULL_REQUEST_ID
)
}
injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL)

View File

@ -28,7 +28,7 @@
"got": "12.6.0",
"next": "14.1.0",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.3",
"nodemailer": "6.9.8",
"openai": "4.28.4",
"qs": "6.11.2",
"react": "18.2.0",
@ -50,7 +50,7 @@
"@typebot.io/variables": "workspace:*",
"@types/cors": "2.8.13",
"@types/node": "20.4.2",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.14",
"@types/papaparse": "5.3.7",
"@types/qs": "6.9.7",
"@types/react": "18.2.15",

View File

@ -0,0 +1,45 @@
import { getMessageStream } from '@typebot.io/bot-engine/apiHandlers/getMessageStream'
import { StreamingTextResponse } from 'ai'
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
const responseHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Expose-Headers': 'Content-Length, X-JSON',
'Access-Control-Allow-Headers': '*',
}
export async function OPTIONS() {
return new Response('ok', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Expose-Headers': 'Content-Length, X-JSON',
'Access-Control-Allow-Headers': '*',
},
})
}
export async function POST(
req: Request,
{ params }: { params: { sessionId: string } }
) {
if (process.env.VERCEL_ENV)
return NextResponse.json(
{ message: "Can't get streaming if hosted on Vercel" },
{ status: 400, headers: responseHeaders }
)
const messages =
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
const { stream, status, message } = await getMessageStream({
sessionId: params.sessionId,
messages,
})
if (!stream)
return NextResponse.json({ message }, { status, headers: responseHeaders })
return new StreamingTextResponse(stream, {
headers: responseHeaders,
})
}

View File

@ -1,14 +1,7 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { continueChatResponseSchema } from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
import { z } from 'zod'
import { filterPotentiallySensitiveLogs } from '@typebot.io/bot-engine/logs/filterPotentiallySensitiveLogs'
import { computeCurrentProgress } from '@typebot.io/bot-engine/computeCurrentProgress'
import { continueChat as continueChatFn } from '@typebot.io/bot-engine/apiHandlers/continueChat'
export const continueChat = publicProcedure
.meta({
@ -29,92 +22,12 @@ export const continueChat = publicProcedure
})
)
.output(continueChatResponseSchema)
.mutation(async ({ input: { sessionId, message }, ctx: { res, origin } }) => {
const session = await getSession(sessionId)
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session not found.',
.mutation(async ({ input: { sessionId, message }, ctx: { origin, res } }) => {
const { corsOrigin, ...response } = await continueChatFn({
origin,
sessionId,
message,
})
}
const isSessionExpired =
session &&
isDefined(session.state.expiryTimeout) &&
session.updatedAt.getTime() + session.state.expiryTimeout < Date.now()
if (isSessionExpired)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session expired. You need to start a new session.',
})
if (
session?.state.allowedOrigins &&
session.state.allowedOrigins.length > 0
) {
if (origin && session.state.allowedOrigins.includes(origin))
res.setHeader('Access-Control-Allow-Origin', origin)
else
res.setHeader(
'Access-Control-Allow-Origin',
session.state.allowedOrigins[0]
)
}
const {
messages,
input,
clientSideActions,
newSessionState,
logs,
lastMessageNewFormat,
visitedEdges,
} = await continueBotFlow(message, {
version: 2,
state: session.state,
startTime: Date.now(),
})
if (newSessionState)
await saveStateToDatabase({
session: {
id: session.id,
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isPreview = isNotDefined(session.state.typebotsQueue[0].resultId)
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ??
0) === 0
return {
messages,
input,
clientSideActions,
dynamicTheme: parseDynamicTheme(newSessionState),
logs: isPreview ? logs : logs?.filter(filterPotentiallySensitiveLogs),
lastMessageNewFormat,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin)
return response
})

View File

@ -1,11 +1,7 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { chatLogSchema } from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { z } from 'zod'
import { saveLogs } from '@typebot.io/bot-engine/queries/saveLogs'
import { formatLogDetails } from '@typebot.io/bot-engine/logs/helpers/formatLogDetails'
import * as Sentry from '@sentry/nextjs'
import { saveClientLogs as saveClientLogsFn } from '@typebot.io/bot-engine/apiHandlers/saveClientLogs'
export const saveClientLogs = publicProcedure
.meta({
@ -22,42 +18,6 @@ export const saveClientLogs = publicProcedure
})
)
.output(z.object({ message: z.string() }))
.mutation(async ({ input: { sessionId, clientLogs } }) => {
const session = await getSession(sessionId)
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session not found.',
})
}
const resultId = session.state.typebotsQueue[0].resultId
if (!resultId) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Result not found.',
})
}
try {
await saveLogs(
clientLogs.map((log) => ({
...log,
resultId,
details: formatLogDetails(log.details),
}))
.mutation(({ input: { sessionId, clientLogs } }) =>
saveClientLogsFn({ sessionId, clientLogs })
)
return {
message: 'Logs successfully saved.',
}
} catch (e) {
console.error('Failed to save logs', e)
Sentry.captureException(e)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to save logs.',
})
}
})

View File

@ -3,11 +3,7 @@ import {
startChatInputSchema,
startChatResponseSchema,
} from '@typebot.io/schemas/features/chat/schema'
import { startSession } from '@typebot.io/bot-engine/startSession'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { filterPotentiallySensitiveLogs } from '@typebot.io/bot-engine/logs/filterPotentiallySensitiveLogs'
import { computeCurrentProgress } from '@typebot.io/bot-engine/computeCurrentProgress'
import { startChat as startChatFn } from '@typebot.io/bot-engine/apiHandlers/startChat'
export const startChat = publicProcedure
.meta({
@ -19,99 +15,11 @@ export const startChat = publicProcedure
})
.input(startChatInputSchema)
.output(startChatResponseSchema)
.mutation(
async ({
input: {
message,
isOnlyRegistering,
publicId,
isStreamEnabled,
prefilledVariables,
resultId: startResultId,
},
ctx: { origin, res },
}) => {
const {
typebot,
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'live',
isOnlyRegistering,
isStreamEnabled,
publicId,
prefilledVariables,
resultId: startResultId,
},
message,
.mutation(async ({ input, ctx: { origin, res } }) => {
const { corsOrigin, ...response } = await startChatFn({
...input,
origin,
})
if (
newSessionState.allowedOrigins &&
newSessionState.allowedOrigins.length > 0
) {
if (origin && newSessionState.allowedOrigins.includes(origin))
res.setHeader('Access-Control-Allow-Origin', origin)
else
res.setHeader(
'Access-Control-Allow-Origin',
newSessionState.allowedOrigins[0]
)
}
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin)
return response
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ??
0) === 0
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
resultId,
dynamicTheme,
logs: logs?.filter(filterPotentiallySensitiveLogs),
clientSideActions,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
}
)

View File

@ -2,11 +2,8 @@ import {
startPreviewChatInputSchema,
startPreviewChatResponseSchema,
} from '@typebot.io/schemas/features/chat/schema'
import { startSession } from '@typebot.io/bot-engine/startSession'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { publicProcedure } from '@/helpers/server/trpc'
import { computeCurrentProgress } from '@typebot.io/bot-engine/computeCurrentProgress'
import { startChatPreview as startChatPreviewFn } from '@typebot.io/bot-engine/apiHandlers/startChatPreview'
export const startChatPreview = publicProcedure
.meta({
@ -32,20 +29,9 @@ export const startChatPreview = publicProcedure
prefilledVariables,
},
ctx: { user },
}) => {
const {
typebot,
messages,
input,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'preview',
}) =>
startChatPreviewFn({
message,
isOnlyRegistering,
isStreamEnabled,
startFrom,
@ -53,54 +39,5 @@ export const startChatPreview = publicProcedure
typebot: startTypebot,
userId: user?.id,
prefilledVariables,
},
message,
})
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ??
0) === 0
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
dynamicTheme,
logs,
clientSideActions,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
}
)

View File

@ -1,14 +1,6 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import {
PublicTypebot,
SessionState,
Typebot,
Variable,
} from '@typebot.io/schemas'
import prisma from '@typebot.io/lib/prisma'
import { updateTypebotInSession as updateTypebotInSessionFn } from '@typebot.io/bot-engine/apiHandlers/updateTypebotInSession'
export const updateTypebotInSession = publicProcedure
.meta({
@ -27,85 +19,6 @@ export const updateTypebotInSession = publicProcedure
})
)
.output(z.object({ message: z.literal('success') }))
.mutation(async ({ input: { sessionId }, ctx: { user } }) => {
if (!user)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
const session = await getSession(sessionId)
if (!session)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Session not found' })
const publicTypebot = (await prisma.publicTypebot.findFirst({
where: {
typebot: {
id: session.state.typebotsQueue[0].typebot.id,
OR: [
{
workspace: {
members: {
some: { userId: user.id, role: { in: ['ADMIN', 'MEMBER'] } },
},
},
},
{
collaborators: {
some: { userId: user.id, type: { in: ['WRITE'] } },
},
},
],
},
},
select: {
edges: true,
groups: true,
variables: true,
},
})) as Pick<PublicTypebot, 'edges' | 'variables' | 'groups'> | null
if (!publicTypebot)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
const newSessionState = updateSessionState(session.state, publicTypebot)
await prisma.chatSession.updateMany({
where: { id: session.id },
data: { state: newSessionState },
})
return { message: 'success' }
})
const updateSessionState = (
currentState: SessionState,
newTypebot: Pick<PublicTypebot, 'edges' | 'variables' | 'groups'>
): SessionState => ({
...currentState,
typebotsQueue: currentState.typebotsQueue.map((typebotInQueue, index) =>
index === 0
? {
...typebotInQueue,
typebot: {
...typebotInQueue.typebot,
edges: newTypebot.edges,
groups: newTypebot.groups,
variables: updateVariablesInSession(
typebotInQueue.typebot.variables,
newTypebot.variables
),
},
}
: typebotInQueue
) as SessionState['typebotsQueue'],
})
const updateVariablesInSession = (
currentVariables: Variable[],
newVariables: Typebot['variables']
): Variable[] => [
...currentVariables,
...newVariables.filter(
(newVariable) =>
!currentVariables.find(
(currentVariable) => currentVariable.id === newVariable.id
.mutation(({ input: { sessionId }, ctx: { user } }) =>
updateTypebotInSessionFn({ user, sessionId })
)
),
]

21
chatApi.Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM oven/bun
WORKDIR /app
COPY . .
RUN apt-get -qy update && apt-get -qy --no-install-recommends install openssl ca-certificates git -y && update-ca-certificates
RUN bun install
# Need Node for Prisma
COPY --from=node:18 /usr/local/bin/node /usr/local/bin/node
RUN bun /app/packages/prisma/scripts/db-exec.ts "bunx prisma generate"
RUN rm -rf /usr/local/bin/node
RUN rm -rf /app/apps/builder
RUN rm -rf /app/apps/viewer
ENV PORT=3000
EXPOSE 3000
CMD ["bun", "run", "apps/chat-api/src/index.ts"]

View File

@ -3,12 +3,20 @@
"name": "typebot-os",
"private": true,
"license": "AGPL-3.0-or-later",
"workspaces": [
"packages/*",
"packages/deprecated/*",
"packages/embeds/*",
"packages/forge/*",
"packages/forge/blocks/*",
"apps/*"
],
"scripts": {
"prepare": "husky install",
"docker:up": "docker compose -f docker-compose.dev.yml up -d && node -e \"setTimeout(() => {}, 5000)\"",
"docker:nuke": "docker compose -f docker-compose.dev.yml down --volumes --remove-orphans",
"lint": "turbo run lint",
"dev": "pnpm docker:up && turbo build --filter=@typebot.io/nextjs... && turbo run dev --filter=builder... --filter=viewer... --parallel --no-cache",
"dev": "pnpm docker:up && turbo build --filter=@typebot.io/nextjs... && turbo run dev --filter=builder... --filter=viewer... --filter=chat-api... --parallel --no-cache",
"build": "pnpm docker:up && turbo run build",
"build:apps": "turbo run build --filter=builder... --filter=viewer...",
"db:migrate": "cd packages/prisma && pnpm run db:migrate",

View File

@ -0,0 +1,102 @@
import { TRPCError } from '@trpc/server'
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
import { getSession } from '../queries/getSession'
import { continueBotFlow } from '../continueBotFlow'
import { filterPotentiallySensitiveLogs } from '../logs/filterPotentiallySensitiveLogs'
import { parseDynamicTheme } from '../parseDynamicTheme'
import { saveStateToDatabase } from '../saveStateToDatabase'
import { computeCurrentProgress } from '../computeCurrentProgress'
type Props = {
origin: string | undefined
message?: string
sessionId: string
}
export const continueChat = async ({ origin, sessionId, message }: Props) => {
const session = await getSession(sessionId)
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session not found.',
})
}
const isSessionExpired =
session &&
isDefined(session.state.expiryTimeout) &&
session.updatedAt.getTime() + session.state.expiryTimeout < Date.now()
if (isSessionExpired)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session expired. You need to start a new session.',
})
let corsOrigin
if (
session?.state.allowedOrigins &&
session.state.allowedOrigins.length > 0
) {
if (origin && session.state.allowedOrigins.includes(origin))
corsOrigin = origin
else corsOrigin = session.state.allowedOrigins[0]
}
const {
messages,
input,
clientSideActions,
newSessionState,
logs,
lastMessageNewFormat,
visitedEdges,
} = await continueBotFlow(message, {
version: 2,
state: session.state,
startTime: Date.now(),
})
if (newSessionState)
await saveStateToDatabase({
session: {
id: session.id,
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isPreview = isNotDefined(session.state.typebotsQueue[0].resultId)
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ?? 0) ===
0
return {
messages,
input,
clientSideActions,
dynamicTheme: parseDynamicTheme(newSessionState),
logs: isPreview ? logs : logs?.filter(filterPotentiallySensitiveLogs),
lastMessageNewFormat,
corsOrigin,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
}

View File

@ -0,0 +1,130 @@
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { OpenAI } from 'openai'
import { decryptV2 } from '@typebot.io/lib/api/encryption/decryptV2'
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
import { ReadOnlyVariableStore } from '@typebot.io/forge'
import {
ParseVariablesOptions,
parseVariables,
} from '@typebot.io/variables/parseVariables'
import { getOpenAIChatCompletionStream } from './legacy/getOpenAIChatCompletionStream'
import { getCredentials } from '../queries/getCredentials'
import { getSession } from '../queries/getSession'
import { getBlockById } from '@typebot.io/schemas/helpers'
import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/helpers'
type Props = {
sessionId: string
messages: OpenAI.Chat.ChatCompletionMessage[] | undefined
}
export const getMessageStream = async ({ sessionId, messages }: Props) => {
const session = await getSession(sessionId)
if (!session?.state || !session.state.currentBlockId)
return { status: 404, message: 'Could not find session' }
const { group, block } = getBlockById(
session.state.currentBlockId,
session.state.typebotsQueue[0].typebot.groups
)
if (!block || !group)
return {
status: 404,
message: 'Could not find block or group',
}
if (!('options' in block))
return {
status: 400,
message: 'This block does not have options',
}
if (block.type === IntegrationBlockType.OPEN_AI && messages) {
try {
const stream = await getOpenAIChatCompletionStream(
session.state,
block.options as ChatCompletionOpenAIOptions,
messages
)
if (!stream)
return {
status: 500,
message: 'Could not create stream',
}
return { stream }
} catch (error) {
if (error instanceof OpenAI.APIError) {
const { message } = error
return {
status: 500,
message,
}
} else {
throw error
}
}
}
if (!isForgedBlockType(block.type))
return {
status: 400,
message: 'This block does not have a stream function',
}
const blockDef = forgedBlocks[block.type]
const action = blockDef?.actions.find((a) => a.name === block.options?.action)
if (!action || !action.run?.stream)
return {
status: 400,
message: 'This block does not have a stream function',
}
try {
if (!block.options.credentialsId)
return { status: 404, message: 'Could not find credentials' }
const credentials = await getCredentials(block.options.credentialsId)
if (!credentials)
return { status: 404, message: 'Could not find credentials' }
const decryptedCredentials = await decryptV2(
credentials.data,
credentials.iv
)
const variables: ReadOnlyVariableStore = {
list: () => session.state.typebotsQueue[0].typebot.variables,
get: (id: string) => {
const variable = session.state.typebotsQueue[0].typebot.variables.find(
(variable) => variable.id === id
)
return variable?.value
},
parse: (text: string, params?: ParseVariablesOptions) =>
parseVariables(
session.state.typebotsQueue[0].typebot.variables,
params
)(text),
}
const stream = await action.run.stream.run({
credentials: decryptedCredentials,
options: block.options,
variables,
})
if (!stream) return { status: 500, message: 'Could not create stream' }
return { stream }
} catch (error) {
if (error instanceof OpenAI.APIError) {
const { message } = error
return {
status: 500,
message,
}
}
return {
status: 500,
message: 'Could not create stream',
}
}
}

View File

@ -0,0 +1,58 @@
import { decryptV2 } from '@typebot.io/lib/api/encryption/decryptV2'
import { isNotEmpty } from '@typebot.io/lib/utils'
import {
ChatCompletionOpenAIOptions,
OpenAICredentials,
} from '@typebot.io/schemas/features/blocks/integrations/openai'
import { SessionState } from '@typebot.io/schemas/features/chat/sessionState'
import { OpenAIStream } from 'ai'
import { parseVariableNumber } from '@typebot.io/variables/parseVariableNumber'
import { ClientOptions, OpenAI } from 'openai'
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
import { getCredentials } from '../../queries/getCredentials'
export const getOpenAIChatCompletionStream = async (
state: SessionState,
options: ChatCompletionOpenAIOptions,
messages: OpenAI.Chat.ChatCompletionMessageParam[]
) => {
if (!options.credentialsId) return
const credentials = await getCredentials(options.credentialsId)
if (!credentials) {
console.error('Could not find credentials in database')
return
}
const { apiKey } = (await decryptV2(
credentials.data,
credentials.iv
)) as OpenAICredentials['data']
const { typebot } = state.typebotsQueue[0]
const temperature = parseVariableNumber(typebot.variables)(
options.advancedSettings?.temperature
)
const config = {
apiKey,
baseURL: options.baseUrl,
defaultHeaders: {
'api-key': apiKey,
},
defaultQuery: isNotEmpty(options.apiVersion)
? {
'api-version': options.apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
const response = await openai.chat.completions.create({
model: options.model ?? defaultOpenAIOptions.model,
temperature,
stream: true,
messages,
})
return OpenAIStream(response)
}

View File

@ -0,0 +1,35 @@
import { WhatsAppWebhookRequestBody } from '@typebot.io/schemas/features/whatsapp'
import { isNotDefined } from '@typebot.io/lib'
import { resumeWhatsAppFlow } from '../whatsapp/resumeWhatsAppFlow'
type Props = {
entry: WhatsAppWebhookRequestBody['entry']
credentialsId: string
workspaceId: string
}
export const receiveMessage = async ({
entry,
credentialsId,
workspaceId,
}: Props) => {
const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0)
if (isNotDefined(receivedMessage)) return { message: 'No message found' }
const contactName =
entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ''
const contactPhoneNumber =
entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ''
const phoneNumberId = entry.at(0)?.changes.at(0)?.value
.metadata.phone_number_id
if (!phoneNumberId) return { message: 'No phone number id found' }
return resumeWhatsAppFlow({
receivedMessage,
sessionId: `wa-${phoneNumberId}-${receivedMessage.from}`,
credentialsId,
workspaceId,
contact: {
name: contactName,
phoneNumber: contactPhoneNumber,
},
})
}

View File

@ -0,0 +1,31 @@
import { WhatsAppWebhookRequestBody } from '@typebot.io/schemas/features/whatsapp'
import { isNotDefined } from '@typebot.io/lib'
import { TRPCError } from '@trpc/server'
import { env } from '@typebot.io/env'
import { resumeWhatsAppFlow } from '../whatsapp/resumeWhatsAppFlow'
type Props = {
entry: WhatsAppWebhookRequestBody['entry']
}
export const receiveMessagePreview = ({ entry }: Props) => {
if (!env.WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'WHATSAPP_PREVIEW_FROM_PHONE_NUMBER_ID is not defined',
})
const receivedMessage = entry.at(0)?.changes.at(0)?.value.messages?.at(0)
if (isNotDefined(receivedMessage)) return { message: 'No message found' }
const contactName =
entry.at(0)?.changes.at(0)?.value?.contacts?.at(0)?.profile?.name ?? ''
const contactPhoneNumber =
entry.at(0)?.changes.at(0)?.value?.messages?.at(0)?.from ?? ''
return resumeWhatsAppFlow({
receivedMessage,
sessionId: `wa-preview-${receivedMessage.from}`,
contact: {
name: contactName,
phoneNumber: contactPhoneNumber,
},
})
}

View File

@ -0,0 +1,49 @@
import { TRPCError } from '@trpc/server'
import { ChatLog } from '@typebot.io/schemas'
import { formatLogDetails } from '../logs/helpers/formatLogDetails'
import { getSession } from '../queries/getSession'
import { saveLogs } from '../queries/saveLogs'
type Props = {
sessionId: string
clientLogs: ChatLog[]
}
export const saveClientLogs = async ({ sessionId, clientLogs }: Props) => {
const session = await getSession(sessionId)
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session not found.',
})
}
const resultId = session.state.typebotsQueue[0].resultId
if (!resultId) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Result not found.',
})
}
try {
await saveLogs(
clientLogs.map((log) => ({
...log,
resultId,
details: formatLogDetails(log.details),
}))
)
return {
message: 'Logs successfully saved.',
}
} catch (e) {
console.error('Failed to save logs', e)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to save logs.',
})
}
}

View File

@ -0,0 +1,107 @@
import { computeCurrentProgress } from '../computeCurrentProgress'
import { filterPotentiallySensitiveLogs } from '../logs/filterPotentiallySensitiveLogs'
import { restartSession } from '../queries/restartSession'
import { saveStateToDatabase } from '../saveStateToDatabase'
import { startSession } from '../startSession'
type Props = {
origin: string | undefined
message?: string
isOnlyRegistering: boolean
publicId: string
isStreamEnabled: boolean
prefilledVariables?: Record<string, unknown>
resultId?: string
}
export const startChat = async ({
origin,
message,
isOnlyRegistering,
publicId,
isStreamEnabled,
prefilledVariables,
resultId: startResultId,
}: Props) => {
const {
typebot,
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'live',
isOnlyRegistering,
isStreamEnabled,
publicId,
prefilledVariables,
resultId: startResultId,
},
message,
})
let corsOrigin
if (
newSessionState.allowedOrigins &&
newSessionState.allowedOrigins.length > 0
) {
if (origin && newSessionState.allowedOrigins.includes(origin))
corsOrigin = origin
else corsOrigin = newSessionState.allowedOrigins[0]
}
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ?? 0) ===
0
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
resultId,
dynamicTheme,
logs: logs?.filter(filterPotentiallySensitiveLogs),
clientSideActions,
corsOrigin,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
}

View File

@ -0,0 +1,97 @@
import { StartFrom, StartTypebot } from '@typebot.io/schemas'
import { restartSession } from '../queries/restartSession'
import { saveStateToDatabase } from '../saveStateToDatabase'
import { startSession } from '../startSession'
import { computeCurrentProgress } from '../computeCurrentProgress'
type Props = {
message?: string
isOnlyRegistering: boolean
isStreamEnabled: boolean
startFrom?: StartFrom
typebotId: string
typebot?: StartTypebot
userId?: string
prefilledVariables?: Record<string, unknown>
}
export const startChatPreview = async ({
message,
isOnlyRegistering,
isStreamEnabled,
startFrom,
typebotId,
typebot: startTypebot,
userId,
prefilledVariables,
}: Props) => {
const {
typebot,
messages,
input,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'preview',
isOnlyRegistering,
isStreamEnabled,
startFrom,
typebotId,
typebot: startTypebot,
userId,
prefilledVariables,
},
message,
})
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
hasCustomEmbedBubble: messages.some(
(message) => message.type === 'custom-embed'
),
})
const isEnded =
newSessionState.progressMetadata &&
!input?.id &&
(clientSideActions?.filter((c) => c.expectsDedicatedReply).length ?? 0) ===
0
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
dynamicTheme,
logs,
clientSideActions,
progress: newSessionState.progressMetadata
? isEnded
? 100
: computeCurrentProgress({
typebotsQueue: newSessionState.typebotsQueue,
progressMetadata: newSessionState.progressMetadata,
currentInputBlockId: input?.id,
})
: undefined,
}
}

View File

@ -0,0 +1,97 @@
import { TRPCError } from '@trpc/server'
import prisma from '@typebot.io/lib/prisma'
import {
SessionState,
Variable,
PublicTypebot,
Typebot,
} from '@typebot.io/schemas'
import { getSession } from '../queries/getSession'
type Props = {
user?: { id: string }
sessionId: string
}
export const updateTypebotInSession = async ({ user, sessionId }: Props) => {
if (!user)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
const session = await getSession(sessionId)
if (!session)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Session not found' })
const publicTypebot = (await prisma.publicTypebot.findFirst({
where: {
typebot: {
id: session.state.typebotsQueue[0].typebot.id,
OR: [
{
workspace: {
members: {
some: { userId: user.id, role: { in: ['ADMIN', 'MEMBER'] } },
},
},
},
{
collaborators: {
some: { userId: user.id, type: { in: ['WRITE'] } },
},
},
],
},
},
select: {
edges: true,
groups: true,
variables: true,
},
})) as Pick<PublicTypebot, 'edges' | 'variables' | 'groups'> | null
if (!publicTypebot)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
const newSessionState = updateSessionState(session.state, publicTypebot)
await prisma.chatSession.updateMany({
where: { id: session.id },
data: { state: newSessionState },
})
return { message: 'success' } as const
}
const updateSessionState = (
currentState: SessionState,
newTypebot: Pick<PublicTypebot, 'edges' | 'variables' | 'groups'>
): SessionState => ({
...currentState,
typebotsQueue: currentState.typebotsQueue.map((typebotInQueue, index) =>
index === 0
? {
...typebotInQueue,
typebot: {
...typebotInQueue.typebot,
edges: newTypebot.edges,
groups: newTypebot.groups,
variables: updateVariablesInSession(
typebotInQueue.typebot.variables,
newTypebot.variables
),
},
}
: typebotInQueue
) as SessionState['typebotsQueue'],
})
const updateVariablesInSession = (
currentVariables: Variable[],
newVariables: Typebot['variables']
): Variable[] => [
...currentVariables,
...newVariables.filter(
(newVariable) =>
!currentVariables.find(
(currentVariable) => currentVariable.id === newVariable.id
)
),
]

View File

@ -84,14 +84,13 @@ export const createChatCompletionOpenAI = async (
)?.name
if (
isPlaneteScale() &&
isCredentialsV2(credentials) &&
newSessionState.isStreamEnabled &&
!newSessionState.whatsApp &&
isNextBubbleMessageWithAssistantMessage(typebot)(
blockId,
assistantMessageVariableName
)
) &&
!process.env.VERCEL_ENV
) {
return {
clientSideActions: [

View File

@ -7,9 +7,9 @@ import {
import got from 'got'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { byId, isDefined, isEmpty } from '@typebot.io/lib'
import prisma from '@typebot.io/lib/prisma'
import { ExecuteIntegrationResponse } from '../../../types'
import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession'
import { getCredentials } from '../../../queries/getCredentials'
import { parseAnswers } from '@typebot.io/results/parseAnswers'
const URL = 'https://api.zemantic.ai/v1/search-documents'
@ -25,11 +25,7 @@ export const executeZemanticAiBlock = async (
outgoingEdgeId: block.outgoingEdgeId,
}
const credentials = await prisma.credentials.findUnique({
where: {
id: block.options?.credentialsId,
},
})
const credentials = await getCredentials(block.options.credentialsId)
if (!credentials) {
return {

View File

@ -19,6 +19,8 @@ import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesI
import { ExecuteIntegrationResponse } from '../types'
import { byId } from '@typebot.io/lib'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { env } from '@typebot.io/env'
import { getCredentials } from '../queries/getCredentials'
export const executeForgedBlock = async (
state: SessionState,
@ -40,11 +42,7 @@ export const executeForgedBlock = async (
logs: [noCredentialsError],
}
}
credentials = await prisma.credentials.findUnique({
where: {
id: block.options.credentialsId,
},
})
credentials = await getCredentials(block.options.credentialsId)
if (!credentials) {
console.error('Could not find credentials in database')
return {
@ -57,15 +55,13 @@ export const executeForgedBlock = async (
const typebot = state.typebotsQueue[0].typebot
if (
action?.run?.stream &&
isPlaneteScale() &&
credentials &&
isCredentialsV2(credentials) &&
state.isStreamEnabled &&
!state.whatsApp &&
isNextBubbleTextWithStreamingVar(typebot)(
block.id,
action.run.stream.getStreamVariableId(block.options)
)
) &&
state.isStreamEnabled &&
!state.whatsApp &&
!process.env.VERCEL_ENV
) {
return {
outgoingEdgeId: block.outgoingEdgeId,

View File

@ -25,9 +25,10 @@
"google-auth-library": "8.9.0",
"google-spreadsheet": "4.1.1",
"got": "12.6.0",
"ky": "^1.1.3",
"libphonenumber-js": "1.10.37",
"node-html-parser": "6.1.5",
"nodemailer": "6.9.3",
"nodemailer": "6.9.8",
"openai": "4.28.4",
"qs": "6.11.2",
"stripe": "12.13.0",
@ -36,7 +37,7 @@
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.14",
"@types/qs": "6.9.7"
}
}

View File

@ -0,0 +1,6 @@
import prisma from '@typebot.io/lib/prisma'
export const getCredentials = async (credentialsId: string) =>
prisma.credentials.findUnique({
where: { id: credentialsId },
})

View File

@ -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, err.response.body)
}
}
@ -172,7 +172,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, err.response.body)
}
}
}
@ -253,7 +253,7 @@ 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, err.response.body)
}
}
}

View File

@ -1,4 +1,4 @@
import got from 'got'
import ky from 'ky'
import {
WhatsAppCredentials,
WhatsAppSendingMessage,
@ -16,8 +16,9 @@ export const sendWhatsAppMessage = async ({
message,
credentials,
}: Props) =>
got.post({
url: `${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}/messages`,
ky.post(
`${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}/messages`,
{
headers: {
Authorization: `Bearer ${credentials.systemUserAccessToken}`,
},
@ -26,4 +27,5 @@ export const sendWhatsAppMessage = async ({
to,
...message,
},
})
}
)

View File

@ -17,11 +17,11 @@
"devDependencies": {
"@faire/mjml-react": "3.3.0",
"@types/node": "20.4.2",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.14",
"@types/react": "18.2.15",
"concurrently": "8.2.0",
"http-server": "14.1.1",
"nodemailer": "6.9.3",
"nodemailer": "6.9.8",
"react": "18.2.0",
"tsx": "3.12.7",
"@typebot.io/lib": "workspace:*",

View File

@ -73,7 +73,8 @@ export const FileUploadForm = (props: Props) => {
})
setIsUploading(true)
const urls = await uploadFiles({
apiHost: props.context.apiHost ?? guessApiHost(),
apiHost:
props.context.apiHost ?? guessApiHost({ ignoreChatApiUrl: true }),
files: [
{
file,
@ -112,7 +113,8 @@ export const FileUploadForm = (props: Props) => {
})
setIsUploading(true)
const urls = await uploadFiles({
apiHost: props.context.apiHost ?? guessApiHost(),
apiHost:
props.context.apiHost ?? guessApiHost({ ignoreChatApiUrl: true }),
files: files.map((file) => ({
file: file,
input: {

View File

@ -25,16 +25,15 @@ export const streamChat =
const apiHost = context.apiHost
const res = await fetch(
`${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/integrations/openai/streamer`,
`${isNotEmpty(apiHost) ? apiHost : guessApiHost()}/api/v1/sessions/${
context.sessionId
}/streamMessage`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sessionId: context.sessionId,
messages,
}),
signal: abortController.signal,

View File

@ -83,7 +83,10 @@ export async function startChatQuery({
startFrom,
typebot,
prefilledVariables,
} satisfies Omit<StartPreviewChatInput, 'typebotId'>,
} satisfies Omit<
StartPreviewChatInput,
'typebotId' | 'isOnlyRegistering'
>,
timeout: false,
}
)

View File

@ -1,6 +1,29 @@
import { getRuntimeVariable } from '@typebot.io/env/getRuntimeVariable'
const cloudViewerUrl = 'https://typebot.io'
const chatApiCloudFallbackHost = 'https://chat.typebot.io'
export const guessApiHost = () =>
getRuntimeVariable('NEXT_PUBLIC_VIEWER_URL')?.split(',')[0] ?? cloudViewerUrl
type Params = {
ignoreChatApiUrl?: boolean
}
export const guessApiHost = (
{ ignoreChatApiUrl }: Params = { ignoreChatApiUrl: false }
) => {
const chatApiUrl = getRuntimeVariable('NEXT_PUBLIC_CHAT_API_URL')
const newChatApiOnUrls = getRuntimeVariable(
'NEXT_PUBLIC_USE_EXPERIMENTAL_CHAT_API_ON'
)
if (
!ignoreChatApiUrl &&
chatApiUrl &&
(!newChatApiOnUrls || newChatApiOnUrls.includes(window.location.href))
) {
return chatApiUrl
}
return (
getRuntimeVariable('NEXT_PUBLIC_VIEWER_URL')?.split(',')[0] ??
chatApiCloudFallbackHost
)
}

17
packages/env/env.ts vendored
View File

@ -46,7 +46,9 @@ const boolean = z.enum(['true', 'false']).transform((value) => value === 'true')
const baseEnv = {
server: {
NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
NODE_ENV: z
.enum(['development', 'staging', 'production', 'test'])
.optional(),
DATABASE_URL: z
.string()
.url()
@ -99,6 +101,15 @@ const baseEnv = {
),
NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID: z.string().min(1).optional(),
NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE: z.coerce.number().optional(),
NEXT_PUBLIC_CHAT_API_URL: z.string().url().optional(),
// To remove to deploy chat API for all typebots
NEXT_PUBLIC_USE_EXPERIMENTAL_CHAT_API_ON: z
.string()
.min(1)
.transform((val) =>
val.split('/').map((s) => s.split(',').map((s) => s.split('|')))
)
.optional(),
NEXT_PUBLIC_VIEWER_404_TITLE: z.string().optional().default('404'),
NEXT_PUBLIC_VIEWER_404_SUBTITLE: z
.string()
@ -114,6 +125,10 @@ const baseEnv = {
NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE: getRuntimeVariable(
'NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE'
),
NEXT_PUBLIC_CHAT_API_URL: getRuntimeVariable('NEXT_PUBLIC_CHAT_API_URL'),
NEXT_PUBLIC_USE_EXPERIMENTAL_CHAT_API_ON: getRuntimeVariable(
'NEXT_PUBLIC_USE_EXPERIMENTAL_CHAT_API_ON'
),
NEXT_PUBLIC_VIEWER_404_TITLE: getRuntimeVariable(
'NEXT_PUBLIC_VIEWER_404_TITLE'
),

View File

@ -15,7 +15,7 @@
"@types/nodemailer": "6.4.8",
"@types/validator": "13.11.9",
"next": "14.1.0",
"nodemailer": "6.9.3",
"nodemailer": "6.9.8",
"tslib": "2.6.0",
"typescript": "5.3.2"
},

View File

@ -4,11 +4,15 @@ import { PrismaClient } from '@typebot.io/prisma'
declare const global: { prisma: PrismaClient }
let prisma: PrismaClient
if (env.NODE_ENV === 'production') {
prisma = new PrismaClient()
if (env.NODE_ENV === 'production' && !process.versions.bun) {
prisma = new PrismaClient({
log: ['info', 'warn', 'error'],
})
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
global.prisma = new PrismaClient({
log: ['info', 'warn', 'error'],
})
}
prisma = global.prisma
}

View File

@ -0,0 +1,7 @@
import { executePrismaCommand } from './executeCommand'
const commandToExecute = process.argv.pop()
if (!commandToExecute) process.exit(1)
executePrismaCommand(commandToExecute, { force: true })

View File

@ -225,14 +225,15 @@ export const startPreviewChatInputSchema = z.object({
.describe(
"[Where to find my bot's ID?](../how-to#how-to-find-my-typebotid)"
),
isStreamEnabled: z.boolean().optional(),
isStreamEnabled: z.boolean().optional().default(false),
message: z.string().optional(),
isOnlyRegistering: z
.boolean()
.optional()
.describe(
'If set to `true`, it will only register the session and not start the bot. This is used for 3rd party chat platforms as it can require a session to be registered before sending the first message.'
),
)
.default(false),
typebot: startTypebotSchema
.optional()
.describe(

View File

@ -174,6 +174,10 @@ export const whatsAppWebhookRequestBodySchema = z.object({
),
})
export type WhatsAppWebhookRequestBody = z.infer<
typeof whatsAppWebhookRequestBodySchema
>
export const whatsAppCredentialsSchema = z
.object({
type: z.literal('whatsApp'),

227
pnpm-lock.yaml generated
View File

@ -211,13 +211,13 @@ importers:
version: 14.1.0(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0)
next-auth:
specifier: 4.22.1
version: 4.22.1(next@14.1.0)(nodemailer@6.9.3)(react-dom@18.2.0)(react@18.2.0)
version: 4.22.1(next@14.1.0)(nodemailer@6.9.8)(react-dom@18.2.0)(react@18.2.0)
nextjs-cors:
specifier: 2.1.2
version: 2.1.2(next@14.1.0)
nodemailer:
specifier: 6.9.3
version: 6.9.3
specifier: 6.9.8
version: 6.9.8
nprogress:
specifier: 0.2.0
version: 0.2.0
@ -328,8 +328,8 @@ importers:
specifier: 20.4.2
version: 20.4.2
'@types/nodemailer':
specifier: 6.4.8
version: 6.4.8
specifier: 6.4.14
version: 6.4.14
'@types/nprogress':
specifier: 0.2.0
version: 0.2.0
@ -370,6 +370,73 @@ importers:
specifier: 3.22.4
version: 3.22.4
apps/chat-api:
dependencies:
'@hono/prometheus':
specifier: 1.0.0
version: 1.0.0(hono@4.0.5)(prom-client@15.1.0)
'@hono/sentry':
specifier: 1.0.1
version: 1.0.1(hono@4.0.5)
'@hono/typebox-validator':
specifier: 0.2.2
version: 0.2.2(@sinclair/typebox@0.32.5)(hono@4.0.5)
'@sinclair/typebox':
specifier: 0.32.5
version: 0.32.5
'@trpc/server':
specifier: 10.40.0
version: 10.40.0
'@typebot.io/bot-engine':
specifier: workspace:*
version: link:../../packages/bot-engine
'@typebot.io/env':
specifier: workspace:*
version: link:../../packages/env
'@typebot.io/forge':
specifier: workspace:*
version: link:../../packages/forge/core
'@typebot.io/forge-repository':
specifier: workspace:*
version: link:../../packages/forge/repository
'@typebot.io/lib':
specifier: workspace:*
version: link:../../packages/lib
'@typebot.io/prisma':
specifier: workspace:*
version: link:../../packages/prisma
'@typebot.io/schemas':
specifier: workspace:*
version: link:../../packages/schemas
'@typebot.io/variables':
specifier: workspace:*
version: link:../../packages/variables
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)
hono:
specifier: 4.0.5
version: 4.0.5
openai:
specifier: 4.28.4
version: 4.28.4
prom-client:
specifier: 15.1.0
version: 15.1.0
devDependencies:
'@typebot.io/tsconfig':
specifier: workspace:*
version: link:../../packages/tsconfig
'@types/react':
specifier: 18.2.15
version: 18.2.15
dotenv-cli:
specifier: 7.2.1
version: 7.2.1
react:
specifier: 18.2.0
version: 18.2.0
apps/docs:
devDependencies:
dotenv-cli:
@ -530,8 +597,8 @@ importers:
specifier: 2.1.2
version: 2.1.2(next@14.1.0)
nodemailer:
specifier: 6.9.3
version: 6.9.3
specifier: 6.9.8
version: 6.9.8
openai:
specifier: 4.28.4
version: 4.28.4
@ -594,8 +661,8 @@ importers:
specifier: 20.4.2
version: 20.4.2
'@types/nodemailer':
specifier: 6.4.8
version: 6.4.8
specifier: 6.4.14
version: 6.4.14
'@types/papaparse':
specifier: 5.3.7
version: 5.3.7
@ -714,6 +781,9 @@ importers:
got:
specifier: 12.6.0
version: 12.6.0
ky:
specifier: ^1.1.3
version: 1.2.0
libphonenumber-js:
specifier: 1.10.37
version: 1.10.37
@ -721,8 +791,8 @@ importers:
specifier: 6.1.5
version: 6.1.5
nodemailer:
specifier: 6.9.3
version: 6.9.3
specifier: 6.9.8
version: 6.9.8
openai:
specifier: 4.28.4
version: 4.28.4
@ -740,8 +810,8 @@ importers:
specifier: workspace:*
version: link:../forge/repository
'@types/nodemailer':
specifier: 6.4.8
version: 6.4.8
specifier: 6.4.14
version: 6.4.14
'@types/qs':
specifier: 6.9.7
version: 6.9.7
@ -894,8 +964,8 @@ importers:
specifier: 20.4.2
version: 20.4.2
'@types/nodemailer':
specifier: 6.4.8
version: 6.4.8
specifier: 6.4.14
version: 6.4.14
'@types/react':
specifier: 18.2.15
version: 18.2.15
@ -912,8 +982,8 @@ importers:
specifier: 14.1.1
version: 14.1.1
nodemailer:
specifier: 6.9.3
version: 6.9.3
specifier: 6.9.8
version: 6.9.8
react:
specifier: 18.2.0
version: 18.2.0
@ -1625,8 +1695,8 @@ importers:
specifier: 14.1.0
version: 14.1.0(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0)
nodemailer:
specifier: 6.9.3
version: 6.9.3
specifier: 6.9.8
version: 6.9.8
tslib:
specifier: 2.6.0
version: 2.6.0
@ -6495,6 +6565,35 @@ packages:
- supports-color
dev: false
/@hono/prometheus@1.0.0(hono@4.0.5)(prom-client@15.1.0):
resolution: {integrity: sha512-cB4TklEw3CVqOdgLYenl6g4TM1YwYimrE044azQmVLLq7arfJfnWupvzev42LM1+ntlhsAAwS0TfpjSdnQBggw==}
peerDependencies:
hono: ^3.12.0
prom-client: ^15.0.0
dependencies:
hono: 4.0.5
prom-client: 15.1.0
dev: false
/@hono/sentry@1.0.1(hono@4.0.5):
resolution: {integrity: sha512-4JgwdyasCQIoH3lhl4yLNxrP4/SElfK01ZV3JUaMvexVJnyAOPuXDhtJasl9Gssg7qDNt8ZIDDjqmVrOwS+AIw==}
peerDependencies:
hono: '>=3.*'
dependencies:
hono: 4.0.5
toucan-js: 3.3.1
dev: false
/@hono/typebox-validator@0.2.2(@sinclair/typebox@0.32.5)(hono@4.0.5):
resolution: {integrity: sha512-6hLnF9Pe+nOWSvX5SIhobZ9Wt7vjTT3sjqDZ5lKALf+9J9XMtNzDyMSjpM5SQv8gg+fOPHu/ZSYxxmUhl65QOg==}
peerDependencies:
'@sinclair/typebox': ^0.31.15
hono: '>=3.9.0'
dependencies:
'@sinclair/typebox': 0.32.5
hono: 4.0.5
dev: false
/@humanwhocodes/config-array@0.11.14:
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
engines: {node: '>=10.10.0'}
@ -7691,6 +7790,11 @@ packages:
/@one-ini/wasm@0.1.1:
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
/@opentelemetry/api@1.8.0:
resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==}
engines: {node: '>=8.0.0'}
dev: false
/@panva/hkdf@1.1.1:
resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==}
dev: false
@ -8803,6 +8907,14 @@ packages:
- supports-color
dev: false
/@sentry/core@7.76.0:
resolution: {integrity: sha512-M+ptkCTeCNf6fn7p2MmEb1Wd9/JXUWxIT/0QEc+t11DNR4FYy1ZP2O9Zb3Zp2XacO7ORrlL3Yc+VIfl5JTgjfw==}
engines: {node: '>=8'}
dependencies:
'@sentry/types': 7.76.0
'@sentry/utils': 7.76.0
dev: false
/@sentry/core@7.77.0:
resolution: {integrity: sha512-Tj8oTYFZ/ZD+xW8IGIsU6gcFXD/gfE+FUxUaeSosd9KHwBQNOLhZSsYo/tTVf/rnQI/dQnsd4onPZLiL+27aTg==}
engines: {node: '>=8'}
@ -8811,6 +8923,16 @@ packages:
'@sentry/utils': 7.77.0
dev: false
/@sentry/integrations@7.76.0:
resolution: {integrity: sha512-4ea0PNZrGN9wKuE/8bBCRrxxw4Cq5T710y8rhdKHAlSUpbLqr/atRF53h8qH3Fi+ec0m38PB+MivKem9zUwlwA==}
engines: {node: '>=8'}
dependencies:
'@sentry/core': 7.76.0
'@sentry/types': 7.76.0
'@sentry/utils': 7.76.0
localforage: 1.10.0
dev: false
/@sentry/integrations@7.77.0:
resolution: {integrity: sha512-P055qXgBHeZNKnnVEs5eZYLdy6P49Zr77A1aWJuNih/EenzMy922GOeGy2mF6XYrn1YJSjEwsNMNsQkcvMTK8Q==}
engines: {node: '>=8'}
@ -8888,11 +9010,23 @@ packages:
'@sentry/utils': 7.77.0
dev: false
/@sentry/types@7.76.0:
resolution: {integrity: sha512-vj6z+EAbVrKAXmJPxSv/clpwS9QjPqzkraMFk2hIdE/kii8s8kwnkBwTSpIrNc8GnzV3qYC4r3qD+BXDxAGPaw==}
engines: {node: '>=8'}
dev: false
/@sentry/types@7.77.0:
resolution: {integrity: sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA==}
engines: {node: '>=8'}
dev: false
/@sentry/utils@7.76.0:
resolution: {integrity: sha512-40jFD+yfQaKpFYINghdhovzec4IEpB7aAuyH/GtE7E0gLpcqnC72r55krEIVILfqIR2Mlr5OKUzyeoCyWAU/yw==}
engines: {node: '>=8'}
dependencies:
'@sentry/types': 7.76.0
dev: false
/@sentry/utils@7.77.0:
resolution: {integrity: sha512-NmM2kDOqVchrey3N5WSzdQoCsyDkQkiRxExPaNI2oKQ/jMWHs9yt0tSy7otPBcXs0AP59ihl75Bvm1tDRcsp5g==}
engines: {node: '>=8'}
@ -8924,6 +9058,10 @@ packages:
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
dev: true
/@sinclair/typebox@0.32.5:
resolution: {integrity: sha512-0M6FyxZwIEu/Ly6W+l7iYqiZQYJ8khLOJGzg+cxivNKRKqk9hctcuDC0UYI7B9vNgycExA8w40m4M3yDKW37RA==}
dev: false
/@sindresorhus/is@5.6.0:
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
engines: {node: '>=14.16'}
@ -9550,6 +9688,12 @@ packages:
resolution: {integrity: sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==}
dev: true
/@types/nodemailer@6.4.14:
resolution: {integrity: sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==}
dependencies:
'@types/node': 20.11.26
dev: true
/@types/nodemailer@6.4.8:
resolution: {integrity: sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==}
dependencies:
@ -11461,6 +11605,10 @@ packages:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
engines: {node: '>=8'}
/bintrees@1.0.2:
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
dev: false
/bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
dependencies:
@ -14934,6 +15082,11 @@ packages:
resolution: {integrity: sha512-KZFBHenkVuyyG4uaqRSXqWJr3HTxcaPguM7rU1BlH/mtbDlzaXNSXTa9AhV+fXEjrNemHu9vtLRIaM8/8OW0xA==}
dev: true
/hono@4.0.5:
resolution: {integrity: sha512-6LEGL1Pf3+dLjVA0NJxAB/3FJ6S3W5qxd/XOG7Wl9YOrpMRZT9lt83R4Ojs8dO6GbAUSutI7zTyjStnSn9sbEg==}
engines: {node: '>=16.0.0'}
dev: false
/hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
@ -16498,6 +16651,11 @@ packages:
engines: {node: '>=18'}
dev: false
/ky@1.2.0:
resolution: {integrity: sha512-dnPW+T78MuJ9tLAiF/apJV7bP7RRRCARXQxsCmsWiKLXqGtMBOgDVOFRYzCAfNe/OrRyFyor5ESgvvC+QWEqOA==}
engines: {node: '>=18'}
dev: false
/language-subtag-registry@0.3.22:
resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==}
dev: false
@ -18206,7 +18364,7 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: false
/next-auth@4.22.1(next@14.1.0)(nodemailer@6.9.3)(react-dom@18.2.0)(react@18.2.0):
/next-auth@4.22.1(next@14.1.0)(nodemailer@6.9.8)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==}
peerDependencies:
next: ^12.2.5 || ^13
@ -18222,7 +18380,7 @@ packages:
cookie: 0.5.0
jose: 4.15.5
next: 14.1.0(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0)
nodemailer: 6.9.3
nodemailer: 6.9.8
oauth: 0.9.15
openid-client: 5.6.5
preact: 10.19.6
@ -18391,8 +18549,8 @@ packages:
/node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
/nodemailer@6.9.3:
resolution: {integrity: sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==}
/nodemailer@6.9.8:
resolution: {integrity: sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==}
engines: {node: '>=6.0.0'}
/nopt@7.2.0:
@ -19539,6 +19697,14 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
/prom-client@15.1.0:
resolution: {integrity: sha512-cCD7jLTqyPdjEPBo/Xk4Iu8jxjuZgZJ3e/oET3L+ZwOuap/7Cw3dH/TJSsZKs1TQLZ2IHpIlRAKw82ef06kmMw==}
engines: {node: ^16 || ^18 || >=20}
dependencies:
'@opentelemetry/api': 1.8.0
tdigest: 0.1.2
dev: false
/promise.series@0.2.0:
resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==}
engines: {node: '>=0.12'}
@ -21542,6 +21708,12 @@ packages:
yallist: 4.0.0
dev: true
/tdigest@0.1.2:
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
dependencies:
bintrees: 1.0.2
dev: false
/terser-webpack-plugin@5.3.10(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.90.3):
resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==}
engines: {node: '>= 10.13.0'}
@ -21671,6 +21843,15 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
/toucan-js@3.3.1:
resolution: {integrity: sha512-9BpkHb/Pzsrtl1ItNq9OEQPnuUHwzce0nV2uG+DYFiQ4fPyiA6mKTBcDwQzcvNkfSER038U+8TzvdkCev+Maww==}
dependencies:
'@sentry/core': 7.76.0
'@sentry/integrations': 7.76.0
'@sentry/types': 7.76.0
'@sentry/utils': 7.76.0
dev: false
/tough-cookie@4.1.3:
resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==}
engines: {node: '>=6'}