⚡ 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:
@ -1,3 +1,3 @@
|
||||
emojiList.json
|
||||
iconNames.ts
|
||||
reporters
|
||||
reporters
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -114,6 +114,7 @@ export const startWhatsAppPreview = authenticatedProcedure
|
||||
typebotId,
|
||||
startFrom,
|
||||
userId: user.id,
|
||||
isStreamEnabled: false,
|
||||
},
|
||||
initialSessionState: {
|
||||
whatsApp: (existingSession?.state as SessionState | undefined)
|
||||
|
36
apps/chat-api/package.json
Normal file
36
apps/chat-api/package.json
Normal 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
30
apps/chat-api/src/auth.ts
Normal 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)
|
30
apps/chat-api/src/index.ts
Normal file
30
apps/chat-api/src/index.ts
Normal 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,
|
||||
}
|
132
apps/chat-api/src/runtimes/web.ts
Normal file
132
apps/chat-api/src/runtimes/web.ts
Normal 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)
|
||||
}
|
||||
)
|
51
apps/chat-api/src/runtimes/whatsapp.ts
Normal file
51
apps/chat-api/src/runtimes/whatsapp.ts
Normal 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)
|
||||
}
|
||||
)
|
8
apps/chat-api/tsconfig.json
Normal file
8
apps/chat-api/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@typebot.io/tsconfig/base.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
@ -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": [
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
@ -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.',
|
||||
})
|
||||
}
|
||||
|
||||
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(),
|
||||
.mutation(async ({ input: { sessionId, message }, ctx: { origin, res } }) => {
|
||||
const { corsOrigin, ...response } = await continueChatFn({
|
||||
origin,
|
||||
sessionId,
|
||||
message,
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
|
@ -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),
|
||||
}))
|
||||
)
|
||||
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.',
|
||||
})
|
||||
}
|
||||
})
|
||||
.mutation(({ input: { sessionId, clientLogs } }) =>
|
||||
saveClientLogsFn({ sessionId, clientLogs })
|
||||
)
|
||||
|
@ -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,
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
: 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,
|
||||
}
|
||||
}
|
||||
)
|
||||
.mutation(async ({ input, ctx: { origin, res } }) => {
|
||||
const { corsOrigin, ...response } = await startChatFn({
|
||||
...input,
|
||||
origin,
|
||||
})
|
||||
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin)
|
||||
return response
|
||||
})
|
||||
|
@ -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,75 +29,15 @@ export const startChatPreview = publicProcedure
|
||||
prefilledVariables,
|
||||
},
|
||||
ctx: { user },
|
||||
}) => {
|
||||
const {
|
||||
typebot,
|
||||
messages,
|
||||
input,
|
||||
dynamicTheme,
|
||||
logs,
|
||||
clientSideActions,
|
||||
newSessionState,
|
||||
visitedEdges,
|
||||
} = await startSession({
|
||||
version: 2,
|
||||
startParams: {
|
||||
type: 'preview',
|
||||
isOnlyRegistering,
|
||||
isStreamEnabled,
|
||||
startFrom,
|
||||
typebotId,
|
||||
typebot: startTypebot,
|
||||
userId: user?.id,
|
||||
prefilledVariables,
|
||||
},
|
||||
}) =>
|
||||
startChatPreviewFn({
|
||||
message,
|
||||
isOnlyRegistering,
|
||||
isStreamEnabled,
|
||||
startFrom,
|
||||
typebotId,
|
||||
typebot: startTypebot,
|
||||
userId: user?.id,
|
||||
prefilledVariables,
|
||||
})
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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
21
chatApi.Dockerfile
Normal 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"]
|
10
package.json
10
package.json
@ -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",
|
||||
|
102
packages/bot-engine/apiHandlers/continueChat.ts
Normal file
102
packages/bot-engine/apiHandlers/continueChat.ts
Normal 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,
|
||||
}
|
||||
}
|
130
packages/bot-engine/apiHandlers/getMessageStream.ts
Normal file
130
packages/bot-engine/apiHandlers/getMessageStream.ts
Normal 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',
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
35
packages/bot-engine/apiHandlers/receiveMessage.ts
Normal file
35
packages/bot-engine/apiHandlers/receiveMessage.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
31
packages/bot-engine/apiHandlers/receiveMessagePreview.ts
Normal file
31
packages/bot-engine/apiHandlers/receiveMessagePreview.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
49
packages/bot-engine/apiHandlers/saveClientLogs.ts
Normal file
49
packages/bot-engine/apiHandlers/saveClientLogs.ts
Normal 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.',
|
||||
})
|
||||
}
|
||||
}
|
107
packages/bot-engine/apiHandlers/startChat.ts
Normal file
107
packages/bot-engine/apiHandlers/startChat.ts
Normal 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,
|
||||
}
|
||||
}
|
97
packages/bot-engine/apiHandlers/startChatPreview.ts
Normal file
97
packages/bot-engine/apiHandlers/startChatPreview.ts
Normal 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,
|
||||
}
|
||||
}
|
97
packages/bot-engine/apiHandlers/updateTypebotInSession.ts
Normal file
97
packages/bot-engine/apiHandlers/updateTypebotInSession.ts
Normal 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
|
||||
)
|
||||
),
|
||||
]
|
@ -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: [
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
6
packages/bot-engine/queries/getCredentials.ts
Normal file
6
packages/bot-engine/queries/getCredentials.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
|
||||
export const getCredentials = async (credentialsId: string) =>
|
||||
prisma.credentials.findUnique({
|
||||
where: { id: credentialsId },
|
||||
})
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import got from 'got'
|
||||
import ky from 'ky'
|
||||
import {
|
||||
WhatsAppCredentials,
|
||||
WhatsAppSendingMessage,
|
||||
@ -16,14 +16,16 @@ export const sendWhatsAppMessage = async ({
|
||||
message,
|
||||
credentials,
|
||||
}: Props) =>
|
||||
got.post({
|
||||
url: `${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}/messages`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${credentials.systemUserAccessToken}`,
|
||||
},
|
||||
json: {
|
||||
messaging_product: 'whatsapp',
|
||||
to,
|
||||
...message,
|
||||
},
|
||||
})
|
||||
ky.post(
|
||||
`${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}/messages`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${credentials.systemUserAccessToken}`,
|
||||
},
|
||||
json: {
|
||||
messaging_product: 'whatsapp',
|
||||
to,
|
||||
...message,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -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:*",
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
|
@ -83,7 +83,10 @@ export async function startChatQuery({
|
||||
startFrom,
|
||||
typebot,
|
||||
prefilledVariables,
|
||||
} satisfies Omit<StartPreviewChatInput, 'typebotId'>,
|
||||
} satisfies Omit<
|
||||
StartPreviewChatInput,
|
||||
'typebotId' | 'isOnlyRegistering'
|
||||
>,
|
||||
timeout: false,
|
||||
}
|
||||
)
|
||||
|
@ -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
17
packages/env/env.ts
vendored
@ -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'
|
||||
),
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
7
packages/prisma/scripts/db-exec.ts
Normal file
7
packages/prisma/scripts/db-exec.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { executePrismaCommand } from './executeCommand'
|
||||
|
||||
const commandToExecute = process.argv.pop()
|
||||
|
||||
if (!commandToExecute) process.exit(1)
|
||||
|
||||
executePrismaCommand(commandToExecute, { force: true })
|
@ -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(
|
||||
|
@ -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
227
pnpm-lock.yaml
generated
@ -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'}
|
||||
|
Reference in New Issue
Block a user