diff --git a/apps/builder/src/features/settings/components/SecurityForm.tsx b/apps/builder/src/features/settings/components/SecurityForm.tsx
new file mode 100644
index 000000000..694a0898f
--- /dev/null
+++ b/apps/builder/src/features/settings/components/SecurityForm.tsx
@@ -0,0 +1,50 @@
+import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
+import { Settings } from '@typebot.io/schemas'
+import React from 'react'
+import { isDefined } from '@typebot.io/lib'
+import { TextInput } from '@/components/inputs'
+import { env } from '@typebot.io/env'
+import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
+import { PrimitiveList } from '@/components/PrimitiveList'
+
+type Props = {
+ security: Settings['security']
+ onUpdate: (security: Settings['security']) => void
+}
+
+export const SecurityForm = ({ security, onUpdate }: Props) => {
+ const updateItems = (items: string[]) => {
+ if (items.length === 0) onUpdate(undefined)
+ onUpdate({
+ allowedOrigins: items.filter(isDefined),
+ })
+ }
+
+ return (
+
+
+
+ Allowed origins
+
+ Restrict the execution of your typebot to specific website origins.
+ By default your bot can be executed on any website.
+
+
+
+ {({ item, onItemChange }) => (
+
+ )}
+
+
+
+ )
+}
diff --git a/apps/builder/src/features/settings/components/SettingsSideMenu.tsx b/apps/builder/src/features/settings/components/SettingsSideMenu.tsx
index 70131322f..0e424e950 100644
--- a/apps/builder/src/features/settings/components/SettingsSideMenu.tsx
+++ b/apps/builder/src/features/settings/components/SettingsSideMenu.tsx
@@ -8,7 +8,12 @@ import {
HStack,
Stack,
} from '@chakra-ui/react'
-import { ChatIcon, CodeIcon, MoreVerticalIcon } from '@/components/icons'
+import {
+ ChatIcon,
+ CodeIcon,
+ LockedIcon,
+ MoreVerticalIcon,
+} from '@/components/icons'
import { Settings } from '@typebot.io/schemas'
import React from 'react'
import { GeneralSettingsForm } from './GeneralSettingsForm'
@@ -16,6 +21,7 @@ import { MetadataForm } from './MetadataForm'
import { TypingEmulationForm } from './TypingEmulationForm'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { headerHeight } from '@/features/editor/constants'
+import { SecurityForm } from './SecurityForm'
export const SettingsSideMenu = () => {
const { typebot, updateTypebot } = useTypebot()
@@ -28,6 +34,12 @@ export const SettingsSideMenu = () => {
updates: { settings: { ...typebot.settings, typingEmulation } },
})
+ const updateSecurity = (security: Settings['security']) =>
+ typebot &&
+ updateTypebot({
+ updates: { settings: { ...typebot.settings, security } },
+ })
+
const handleGeneralSettingsChange = (general: Settings['general']) =>
typebot &&
updateTypebot({ updates: { settings: { ...typebot.settings, general } } })
@@ -85,6 +97,23 @@ export const SettingsSideMenu = () => {
)}
+
+
+
+
+ Security
+
+
+
+
+ {typebot && (
+
+ )}
+
+
diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json
index 88cf98de3..8c1d0e68c 100644
--- a/apps/docs/openapi/builder.json
+++ b/apps/docs/openapi/builder.json
@@ -27457,6 +27457,17 @@
"type": "boolean"
}
}
+ },
+ "security": {
+ "type": "object",
+ "properties": {
+ "allowedOrigins": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
},
"title": "Settings"
diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json
index 110330e94..900e7b52c 100644
--- a/apps/docs/openapi/viewer.json
+++ b/apps/docs/openapi/viewer.json
@@ -7679,6 +7679,17 @@
"type": "boolean"
}
}
+ },
+ "security": {
+ "type": "object",
+ "properties": {
+ "allowedOrigins": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
},
"title": "Settings"
diff --git a/apps/docs/settings/overview.mdx b/apps/docs/settings/overview.mdx
index 40ac26223..b7e395700 100644
--- a/apps/docs/settings/overview.mdx
+++ b/apps/docs/settings/overview.mdx
@@ -36,6 +36,17 @@ return new Promise((res) => setTimeout(res, 3000))
You can tweak `3000` (3s) to your liking.
+## Security
+
+By default, your typebot can be executed from any origin but you can restrict the execution of your typebot to specific origins. This is useful if you want to embed your typebot in your website and prevent it from being executed on other websites by malicious actors.
+
+For example, if you want to allow your typebot to be executed only on `https://my-company.com`, you can add `https://my-company.com` to the list of allowed origins.
+
+
+ If you add a URL to the list but omit https://typebot.co, then your typebot
+ shareable URL will not work anymore.
+
+
## Metadata
In the Metadata section, you can customize how the preview card will look if you share your bot URL on social media for example.
diff --git a/apps/viewer/src/features/chat/api/continueChat.ts b/apps/viewer/src/features/chat/api/continueChat.ts
index 9ce28e0f5..03b666dd5 100644
--- a/apps/viewer/src/features/chat/api/continueChat.ts
+++ b/apps/viewer/src/features/chat/api/continueChat.ts
@@ -28,7 +28,7 @@ export const continueChat = publicProcedure
})
)
.output(continueChatResponseSchema)
- .mutation(async ({ input: { sessionId, message } }) => {
+ .mutation(async ({ input: { sessionId, message }, ctx: { res, origin } }) => {
const session = await getSession(sessionId)
if (!session) {
@@ -49,6 +49,15 @@ export const continueChat = publicProcedure
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.removeHeader('Access-Control-Allow-Origin')
+ }
+
const {
messages,
input,
diff --git a/apps/viewer/src/features/chat/api/startChat.ts b/apps/viewer/src/features/chat/api/startChat.ts
index 2622ff912..b2445d13c 100644
--- a/apps/viewer/src/features/chat/api/startChat.ts
+++ b/apps/viewer/src/features/chat/api/startChat.ts
@@ -28,6 +28,7 @@ export const startChat = publicProcedure
prefilledVariables,
resultId: startResultId,
},
+ ctx: { origin, res },
}) => {
const {
typebot,
@@ -52,6 +53,15 @@ export const startChat = publicProcedure
message,
})
+ if (
+ newSessionState.allowedOrigins &&
+ newSessionState.allowedOrigins.length > 0
+ ) {
+ if (origin && newSessionState.allowedOrigins.includes(origin))
+ res.setHeader('Access-Control-Allow-Origin', origin)
+ else res.removeHeader('Access-Control-Allow-Origin')
+ }
+
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
diff --git a/apps/viewer/src/helpers/server/context.ts b/apps/viewer/src/helpers/server/context.ts
index c5250f16f..866431f63 100644
--- a/apps/viewer/src/helpers/server/context.ts
+++ b/apps/viewer/src/helpers/server/context.ts
@@ -11,6 +11,8 @@ export async function createContext(opts: trpcNext.CreateNextContextOptions) {
return {
user,
+ origin: opts.req.headers.origin,
+ res: opts.res,
}
}
diff --git a/packages/bot-engine/startSession.ts b/packages/bot-engine/startSession.ts
index e64ad2618..9a40aadec 100644
--- a/packages/bot-engine/startSession.ts
+++ b/packages/bot-engine/startSession.ts
@@ -131,6 +131,10 @@ export const startSession = async ({
dynamicTheme: parseDynamicThemeInState(typebot.theme),
isStreamEnabled: startParams.isStreamEnabled,
typingEmulation: typebot.settings.typingEmulation,
+ allowedOrigins:
+ startParams.type === 'preview'
+ ? undefined
+ : typebot.settings.security?.allowedOrigins,
...initialSessionState,
}
diff --git a/packages/schemas/features/chat/sessionState.ts b/packages/schemas/features/chat/sessionState.ts
index 57c7c4244..9ef4bb72b 100644
--- a/packages/schemas/features/chat/sessionState.ts
+++ b/packages/schemas/features/chat/sessionState.ts
@@ -88,6 +88,7 @@ const sessionStateSchemaV3 = sessionStateSchemaV2
.extend({
version: z.literal('3'),
currentBlockId: z.string().optional(),
+ allowedOrigins: z.array(z.string()).optional(),
})
export type SessionState = z.infer
diff --git a/packages/schemas/features/typebot/settings/schema.ts b/packages/schemas/features/typebot/settings/schema.ts
index 22d71f14e..3982c3801 100644
--- a/packages/schemas/features/typebot/settings/schema.ts
+++ b/packages/schemas/features/typebot/settings/schema.ts
@@ -42,6 +42,11 @@ export const settingsSchema = z
isEnabled: z.boolean().optional(),
})
.optional(),
+ security: z
+ .object({
+ allowedOrigins: z.array(z.string()).optional(),
+ })
+ .optional(),
})
.openapi({
title: 'Settings',
diff --git a/packages/variables/deepParseVariables.ts b/packages/variables/deepParseVariables.ts
index cdc14d167..1b1d9b9fa 100644
--- a/packages/variables/deepParseVariables.ts
+++ b/packages/variables/deepParseVariables.ts
@@ -20,9 +20,11 @@ export const deepParseVariables =
},
parseVariablesOptions: ParseVariablesOptions = defaultParseVariablesOptions
) =>
- >(object: T): T =>
- Object.keys(object).reduce((newObj, key) => {
- const currentValue = object[key]
+ (object: T): T => {
+ if (!object) return object as T
+ if (typeof object !== 'object') return object as T
+ return Object.keys(object).reduce((newObj, key) => {
+ const currentValue = (object as Record)[key]
if (typeof currentValue === 'string') {
const parsedVariable = parseVariables(
@@ -63,3 +65,4 @@ export const deepParseVariables =
return { ...newObj, [key]: currentValue }
}, {} as T)
+ }