Compare commits

..

6 Commits

Author SHA1 Message Date
TheodorTomas
5ad3c009c2 WIP squash this commit 2026-02-09 18:28:40 +07:00
Theodór Tómas
202958cac2 fix: replace @vercel/og with next/og (#7208) 2026-02-06 04:53:42 +00:00
Harsh Bhat
8e901fb3c9 docs: Validation Rules (#7213) 2026-02-05 14:51:26 +00:00
Harsh Bhat
29afb3e4e9 docs: Formbricks Hubspot integration (#7212) 2026-02-05 12:30:05 +00:00
Matti Nannt
38a3b31761 fix: upgrade preact to fix JSON VNode Injection vulnerability (#7209)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 11:10:12 +00:00
Dhruwang Jariwala
2bfb79d999 fix: translation github action (#7207) 2026-02-05 11:06:21 +00:00
26 changed files with 515 additions and 194 deletions

View File

@@ -32,21 +32,20 @@ jobs:
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node.js
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
node-version: 18
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Setup pnpm
uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
- name: Setup Node.js 22.x
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
version: 9.15.9
node-version: 22.x
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Validate translation keys
run: |

View File

@@ -38,7 +38,7 @@ export const replaceAttributeRecall = (survey: TSurvey, attributes: TAttributes)
}
});
}
(surveyTemp.endings ?? []).forEach((ending) => {
surveyTemp.endings.forEach((ending) => {
if (ending.type === "endScreen") {
languages.forEach((language) => {
if (ending.headline && ending.headline[language]?.includes("recall:")) {

View File

@@ -1,4 +1,4 @@
import { ImageResponse } from "@vercel/og";
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const GET = async (req: NextRequest) => {

View File

@@ -110,7 +110,7 @@ export const QuotaModal = ({
],
},
action: quota?.action || "endSurvey",
endingCardId: quota?.endingCardId || survey.endings?.[0]?.id || null,
endingCardId: quota?.endingCardId || survey.endings[0]?.id || null,
countPartialSubmissions: quota?.countPartialSubmissions || false,
surveyId: survey.id,
};

View File

@@ -36,6 +36,7 @@ import {
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { Input } from "@/modules/ui/components/input";
import { InputCombobox } from "@/modules/ui/components/input-combo-box";
import {
Select,
SelectContent,
@@ -187,14 +188,16 @@ export const TeamSettingsModal = ({
const currentMemberId = watchMembers[index]?.userId;
return orgMembers
.filter((om) => !selectedMemberIds.includes(om?.id) || om?.id === currentMemberId)
.map((om) => ({ label: om?.name, value: om?.id }));
.map((om) => ({ label: om?.name, value: om?.id }))
.sort((a, b) => a.label.localeCompare(b.label));
};
const getProjectOptionsForIndex = (index: number) => {
const currentProjectId = watchProjects[index]?.projectId;
return orgProjects
.filter((op) => !selectedProjectIds.includes(op?.id) || op?.id === currentProjectId)
.map((op) => ({ label: op?.name, value: op?.id }));
.map((op) => ({ label: op?.name, value: op?.id }))
.sort((a, b) => a.label.localeCompare(b.label));
};
const handleMemberSelectionChange = (index: number, userId: string) => {
@@ -278,29 +281,21 @@ export const TeamSettingsModal = ({
return (
<FormItem className="flex-1">
<Select
onValueChange={(val) => {
field.onChange(val);
handleMemberSelectionChange(index, val);
<InputCombobox
id={`member-${index}-select`}
options={memberOpts}
value={member.userId || null}
onChangeValue={(val) => {
const userId = val as string;
field.onChange(userId);
handleMemberSelectionChange(index, userId);
}}
disabled={isSelectDisabled}
value={member.userId}>
<SelectTrigger>
<SelectValue
placeholder={t("environments.settings.teams.select_member")}
/>
</SelectTrigger>
<SelectContent>
{memberOpts.map((option) => (
<SelectItem
key={option.value}
value={option.value}
id={`member-${index}-option`}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
disabled={!!isSelectDisabled}
comboboxClasses="max-w-full"
searchPlaceholder={t(
"environments.settings.teams.select_member"
)}
/>
{error?.message && (
<FormError className="text-left">{error.message}</FormError>
)}
@@ -426,26 +421,19 @@ export const TeamSettingsModal = ({
return (
<FormItem className="flex-1">
<Select
onValueChange={field.onChange}
value={project.projectId}
disabled={isSelectDisabled}>
<SelectTrigger>
<SelectValue
placeholder={t("environments.settings.teams.select_workspace")}
/>
</SelectTrigger>
<SelectContent>
{projectOpts.map((option) => (
<SelectItem
key={option.value}
value={option.value}
id={`project-${index}-option`}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<InputCombobox
id={`project-${index}-select`}
options={projectOpts}
value={project.projectId || null}
onChangeValue={(val) => {
field.onChange(val as string);
}}
disabled={!!isSelectDisabled}
comboboxClasses="max-w-full"
searchPlaceholder={t(
"environments.settings.teams.select_workspace"
)}
/>
{error?.message && (
<FormError className="text-left">{error.message}</FormError>
)}

View File

@@ -115,7 +115,7 @@ export const ElementFormInput = ({
: currentElement.id;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isWelcomeCard, isEndingCard, currentElement?.id]);
const endingCard = (localSurvey.endings ?? []).find((ending) => ending.id === elementId);
const endingCard = localSurvey.endings.find((ending) => ending.id === elementId);
const surveyLanguageCodes = useMemo(
() => extractLanguageCodes(localSurvey.languages),

View File

@@ -57,7 +57,7 @@ export const getEndingCardText = (
questionIdx: number
): TI18nString => {
const endingCardIndex = questionIdx - questions.length;
const card = survey.endings?.[endingCardIndex];
const card = survey.endings[endingCardIndex];
if (card?.type === "endScreen") {
return (card[id as keyof typeof card] as TI18nString) || createI18nString("", surveyLanguageCodes);

View File

@@ -85,7 +85,7 @@ export const EditorCardMenu = ({
const elements = getElementsFromBlocks(survey.blocks);
const isDeleteDisabled =
cardType === "element" ? elements.length === 1 : survey.type === "link" && (survey.endings?.length ?? 0) === 1;
cardType === "element" ? elements.length === 1 : survey.type === "link" && survey.endings.length === 1;
const availableElementTypes = isCxMode ? getCXElementNameMap(t) : getElementNameMap(t);

View File

@@ -102,42 +102,6 @@ describe("Survey Utils", () => {
expect(result.segment).toBeNull();
expect(result.id).toBe("surveyJs");
});
test("should default endings to empty array when null", () => {
const surveyPrisma = {
id: "survey5",
name: "Survey with null endings",
displayPercentage: "100",
segment: null,
endings: null,
};
const result = transformPrismaSurvey<TSurvey>(surveyPrisma);
expect(result.endings).toEqual([]);
});
test("should default endings to empty array when undefined", () => {
const surveyPrisma = {
id: "survey6",
name: "Survey with undefined endings",
displayPercentage: "100",
segment: null,
endings: undefined,
};
const result = transformPrismaSurvey<TSurvey>(surveyPrisma);
expect(result.endings).toEqual([]);
});
test("should preserve endings when provided", () => {
const surveyPrisma = {
id: "survey7",
name: "Survey with endings",
displayPercentage: "100",
segment: null,
endings: [{ id: "ending1", type: "endScreen" }],
};
const result = transformPrismaSurvey<TSurvey>(surveyPrisma);
expect(result.endings).toEqual([{ id: "ending1", type: "endScreen" }]);
});
});
describe("buildWhereClause", () => {

View File

@@ -21,8 +21,6 @@ export const transformPrismaSurvey = <T extends TSurvey | TJsEnvironmentStateSur
displayPercentage: Number(surveyPrisma.displayPercentage) || null,
segment,
customHeadScriptsMode: surveyPrisma.customHeadScriptsMode,
// Ensure endings is always an array to prevent runtime errors
endings: surveyPrisma.endings ?? [],
} as T;
return transformedSurvey;

View File

@@ -64,6 +64,7 @@ export interface InputComboboxProps {
showCheckIcon?: boolean;
comboboxClasses?: string;
emptyDropdownText?: string;
disabled?: boolean;
}
// Helper to flatten all options and their children
@@ -87,6 +88,7 @@ export const InputCombobox: React.FC<InputComboboxProps> = ({
showCheckIcon = false,
comboboxClasses,
emptyDropdownText,
disabled = false,
}) => {
const { t } = useTranslation();
const resolvedSearchPlaceholder = searchPlaceholder ?? t("common.search");
@@ -201,6 +203,7 @@ export const InputCombobox: React.FC<InputComboboxProps> = ({
<div
className={cn(
"group/icon flex max-w-[440px] overflow-hidden rounded-md border border-slate-300 hover:border-slate-400",
disabled && "pointer-events-none opacity-50",
comboboxClasses
)}>
{withInput && inputType !== "dropdown" && (
@@ -213,7 +216,7 @@ export const InputCombobox: React.FC<InputComboboxProps> = ({
/>
)}
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenu open={open} onOpenChange={(o) => !disabled && setOpen(o)}>
<DropdownMenuTrigger asChild className="z-10">
<div
id={id}

View File

@@ -102,7 +102,7 @@ export const PreviewSurvey = ({
}
// check the endings
const ending = (survey.endings ?? []).find((e) => e.id === newElementId);
const ending = survey.endings.find((e) => e.id === newElementId);
if (ending) {
setBlockId(ending.id);
return;
@@ -119,7 +119,7 @@ export const PreviewSurvey = ({
const onFinished = () => {
// close modal if there are no elements left
if (survey.type === "app" && (survey.endings?.length ?? 0) === 0) {
if (survey.type === "app" && survey.endings.length === 0) {
setIsModalOpen(false);
setTimeout(() => {
if (survey.blocks[0]) {

View File

@@ -80,7 +80,8 @@
"xm-and-surveys/surveys/general-features/email-followups",
"xm-and-surveys/surveys/general-features/quota-management",
"xm-and-surveys/surveys/general-features/spam-protection",
"xm-and-surveys/surveys/general-features/tags"
"xm-and-surveys/surveys/general-features/tags",
"xm-and-surveys/surveys/general-features/validation-rules"
]
},
{
@@ -161,6 +162,7 @@
"xm-and-surveys/core-features/integrations/activepieces",
"xm-and-surveys/core-features/integrations/airtable",
"xm-and-surveys/core-features/integrations/google-sheets",
"xm-and-surveys/core-features/integrations/hubspot",
"xm-and-surveys/core-features/integrations/make",
"xm-and-surveys/core-features/integrations/n8n",
"xm-and-surveys/core-features/integrations/notion",

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,183 @@
---
title: "HubSpot"
description: "Learn how to integrate Formbricks with HubSpot to automatically create or update contacts when survey responses are submitted."
---
<Note>
Formbricks doesn't have a native HubSpot integration yet. This guide shows you how to connect Formbricks with HubSpot using automation platforms (Make.com, n8n) or custom webhooks.
</Note>
## Prerequisites
Before setting up the integration, you'll need:
1. **A Formbricks account** with at least one survey that collects email addresses
2. **A HubSpot account** with API access
3. **HubSpot API credentials** (see authentication options below)
4. **Automation service** like Make, n8n, or ActivePieces (for no-code methods)
### HubSpot Authentication Options
HubSpot offers two main ways to authenticate API requests:
#### Option A: OAuth 2.0 via Public App (Recommended)
OAuth is the recommended approach for production integrations. When using Make.com or n8n, they handle OAuth authentication for you through their built-in HubSpot connectors.
#### Option B: Legacy Private Apps (Simple Setup)
For custom webhook handlers, you can use a Legacy Private App which provides a static access token. While marked as "legacy," these apps remain fully supported by HubSpot.
To create a Legacy Private App:
<Steps>
<Step title="Open HubSpot Settings">
Go to your HubSpot account **Settings** and navigate to **Integrations** → **Private Apps**.
</Step>
<Step title="Create the App">
Click **Create a private app** and give it a name (e.g., "Formbricks Integration").
</Step>
<Step title="Configure Scopes">
Under **Scopes**, add `crm.objects.contacts.write` and `crm.objects.contacts.read`.
</Step>
<Step title="Get Your Access Token">
Click **Create app** and copy the access token.
</Step>
</Steps>
<Note>
For more information on HubSpot's authentication options, see the [HubSpot Authentication Overview](https://developers.hubspot.com/docs/guides/apps/authentication/intro-to-auth).
</Note>
---
## Method 1: Using Make.com (Recommended for No-Code)
<Note>
Before starting, ensure your survey has clear `questionId` values set. You can only update these before publishing. If your survey is already published, duplicate it and update the question IDs in the copy.
</Note>
<Steps>
<Step title="Set Up Your Survey">
Make sure your survey has meaningful `questionId` values for each question. This makes mapping responses to HubSpot fields easier.
![Update Question ID](/images/xm-and-surveys/core-features/integrations/make/update-question-id.webp)
</Step>
<Step title="Create a New Make.com Scenario">
Go to [Make.com](https://make.com) and create a new scenario. Search for **Formbricks** and select it as your trigger, then choose **Response Finished** as the trigger event.
![Search Formbricks](/images/xm-and-surveys/core-features/integrations/make/search-formbricks.webp)
</Step>
<Step title="Connect Formbricks to Make">
Click **Create a webhook**, enter your Formbricks API Host (default: `https://app.formbricks.com`), add your Formbricks API Key (see [API Key Setup](/api-reference/rest-api#how-to-generate-an-api-key)), and select the survey you want to connect.
![Enter API Key](/images/xm-and-surveys/core-features/integrations/make/enter-api-key-and-host.webp)
</Step>
<Step title="Add the HubSpot Module">
Click the **+** button after the Formbricks trigger, search for **HubSpot**, choose **Create or Update a Contact** as the action, and connect your HubSpot account.
</Step>
<Step title="Map Formbricks Fields to HubSpot">
Map the Formbricks response fields to HubSpot contact properties:
| HubSpot Field | Formbricks Field |
| ------------- | ---------------- |
| Email | `data.email` (your email question ID) |
| First Name | `data.firstName` (if collected) |
| Last Name | `data.lastName` (if collected) |
| Custom Property | Any other `data.*` field |
You can also map metadata: `meta.country`, `meta.userAgent.browser`, `survey.title`.
</Step>
<Step title="Test and Activate">
Submit a test response to your Formbricks survey, verify the contact appears in HubSpot, and turn on your Make scenario.
</Step>
</Steps>
---
## Method 2: Using n8n (Self-Hosted Option)
<Note>
The Formbricks n8n node is available as a community node. Install it via **Settings** → **Community Nodes** → install `@formbricks/n8n-nodes-formbricks`.
</Note>
<Steps>
<Step title="Set Up the Formbricks Trigger">
Create a new workflow in n8n, add the **Formbricks** trigger node, connect it with your Formbricks API Key and host, select **Response Finished** as the event, and choose your survey.
![Add Formbricks Trigger](/images/xm-and-surveys/core-features/integrations/n8n/add-formbricks-trigger.webp)
</Step>
<Step title="Add the HubSpot Node">
Add a new node and search for **HubSpot**, select **Create/Update Contact** as the operation, and connect your HubSpot account (n8n supports both OAuth and access token authentication).
</Step>
<Step title="Configure Field Mapping">
In the HubSpot node, map the fields:
```
Email: {{ $json.data.email }}
First Name: {{ $json.data.firstName }}
Last Name: {{ $json.data.lastName }}
```
For custom HubSpot properties, use the **Additional Fields** section to add mappings like `survey_source`, `response_id`, and `submission_date`.
</Step>
<Step title="Test Your Workflow">
Click **Listen for event** in the Formbricks trigger, submit a test survey response, verify the data flows through to HubSpot, and activate your workflow.
</Step>
</Steps>
---
## Method 3: Using Webhooks (Custom Integration)
For maximum flexibility, you can use Formbricks webhooks with a custom endpoint that calls the HubSpot API directly. This approach is ideal for developers who want full control.
<Note>
This method requires a HubSpot access token. You can use a Legacy Private App token (simplest) or implement OAuth 2.0 for production applications.
</Note>
<Steps>
<Step title="Create a Formbricks Webhook">
Go to **Configuration** → **Integrations** in Formbricks, click **Manage Webhooks** → **Add Webhook**, enter your endpoint URL, select **Response Finished** as the trigger, and choose the surveys to monitor.
![Integrations Tab](/images/xm-and-surveys/core-features/integrations/webhooks/integrations-tab.webp)
</Step>
<Step title="Build Your Webhook Handler">
Your webhook handler needs to:
- **Receive the Formbricks webhook** - Accept POST requests with the survey response payload
- **Extract contact data** - Parse the email and other fields from `data.data` (keyed by your `questionId` values)
- **Call the HubSpot API** - Use the [HubSpot Contacts API](https://developers.hubspot.com/docs/api/crm/contacts) to create or update contacts
- **Handle duplicates** - HubSpot returns a 409 error if a contact with that email exists; search and update instead
You can deploy using serverless functions (Vercel, AWS Lambda, Cloudflare Workers), traditional servers, or low-code platforms. For webhook signature verification, see the [Webhooks documentation](/xm-and-surveys/core-features/integrations/webhooks).
</Step>
<Step title="Deploy and Test">
Deploy your webhook handler to a publicly accessible URL, add the URL to your Formbricks webhook configuration, submit a test survey response, and verify the contact appears in HubSpot.
</Step>
</Steps>
---
## Troubleshooting
### Contact Not Created in HubSpot
1. **Check the email field**: Ensure your survey has an email question and you're mapping the correct `questionId`
2. **Verify API token**: Make sure your HubSpot access token has the required scopes (`crm.objects.contacts.write` and `crm.objects.contacts.read`)
3. **Check for duplicates**: HubSpot returns a 409 error if a contact with that email already exists
### Webhook Not Triggering
1. Verify the webhook URL is publicly accessible
2. Check that **Response Finished** trigger is selected
3. Ensure the survey is linked to the webhook
### Testing Your Integration
1. Use a unique test email for each test
2. Check HubSpot's **Contacts** page after submitting a response
3. Review your webhook handler logs for errors
---
Still struggling or something not working as expected? [Join our GitHub Discussions](https://github.com/formbricks/formbricks/discussions) and we're happy to help!

View File

@@ -15,6 +15,8 @@ At Formbricks, we understand the importance of integrating with third-party appl
* [Google Sheets](/xm-and-surveys/core-features/integrations/google-sheets): Automatically send responses to a Google Sheet of your choice.
* [HubSpot](/xm-and-surveys/core-features/integrations/hubspot): Create or update HubSpot contacts automatically when survey responses are submitted.
* [Make](/xm-and-surveys/core-features/integrations/make): Leverage Make's powerful automation capabilities to automate your workflows.
* [n8n](/xm-and-surveys/core-features/integrations/n8n)(Open Source): Automate workflows with n8n's no-code automation tool

View File

@@ -0,0 +1,185 @@
---
title: "Validation Rules"
description: "Validation rules help you ensure that respondents provide data in the correct format and within expected constraints"
icon: "check-double"
---
By adding validation rules to your questions, you can improve data quality, reduce errors, and create a better survey experience.
![Validation Rules Editor](/images/xm-and-surveys/core-features/validation-rules/editor.webp)
## How Validation Rules Work
Validation rules are evaluated when a respondent submits their answer. If the answer doesn't meet the validation criteria, an error message is displayed and the respondent must correct their input before proceeding.
You can combine multiple validation rules using **All are true** or **Any is true** logic:
- **All are true**: All rules must pass for the response to be valid
- **Any is true**: At least one rule must pass for the response to be valid
## Available Validation Rules by Question Type
### Free Text Questions
Free text questions support different validation rules based on the input type:
#### Text Input Type
| Rule | Description | Example |
|------|-------------|---------|
| At least (characters) | Requires at least N characters | At least 10 characters for detailed feedback |
| At most (characters) | Limits response to N characters | At most 500 characters for short answers |
| Matches Regex Pattern | Matches a regular expression pattern | Custom format validation |
| Is | Exact match required | Must equal "CONFIRM" |
| Is not | Must not match the value | Cannot be "N/A" |
| Contains | Must include the substring | Must contain "@company.com" |
| Does not contain | Must not include the substring | Cannot contain profanity |
#### Email Input Type
Email input automatically validates email format. Additional rules available:
- At least, At most (characters)
- Matches Regex Pattern, Is, Is not
- Contains, Does not contain
#### URL Input Type
URL input automatically validates URL format. Additional rules available:
- At least, At most (characters)
- Matches Regex Pattern, Is, Is not
- Contains, Does not contain
#### Phone Input Type
Phone input automatically validates phone number format. Additional rules available:
- At least, At most (characters)
- Matches Regex Pattern, Is, Is not
- Contains, Does not contain
#### Number Input Type
| Rule | Description | Example |
|------|-------------|---------|
| At least | Number must be at least N | Age must be at least 18 |
| At most | Number cannot exceed N | Quantity at most 100 |
| Is | Number must equal N | Quantity is 1 |
| Is not | Number must not equal N | Cannot be 0 |
### Multiple Choice (Multi-Select) Questions
| Rule | Description | Example |
|------|-------------|---------|
| At least (options selected) | Require at least N selections | At least 2 options selected |
| At most (options selected) | Limit to N selections | At most 3 options selected |
### Picture Selection Questions
| Rule | Description | Example |
|------|-------------|---------|
| At least (options selected) | Require at least N pictures | At least 1 design selected |
| At most (options selected) | Limit to N pictures | At most 2 favorites selected |
### Date Questions
| Rule | Description | Example |
|------|-------------|---------|
| Is later than | Date must be after specified date | Must be after today |
| Is earlier than | Date must be before specified date | Must be before Dec 31, 2025 |
| Is between | Date must be within range | Between Jan 1 and Dec 31 |
| Is not between | Date must be outside range | Cannot be during holidays |
<Note>
Date values should be specified in YYYY-MM-DD format (e.g., 2025-01-15).
</Note>
### Matrix Questions
| Rule | Description | Example |
|------|-------------|---------|
| Minimum rows answered | Require at least N rows to be answered | Answer at least 3 rows |
| Answer all rows | All rows must have a selection | Complete the entire matrix |
### Ranking Questions
| Rule | Description | Example |
|------|-------------|---------|
| Minimum options ranked | Require at least N items to be ranked | Rank your top 3 |
| Rank all options | All options must be ranked | Rank all 5 items |
### File Upload Questions
| Rule | Description | Example |
|------|-------------|---------|
| File extension is | Only allow specific file types | Only .pdf, .docx allowed |
| File extension is not | Block specific file types | No .exe files |
<Note>
File size limits are configured separately in the question settings using the "Maximum file size" option.
</Note>
### Address Questions
Each address field (Address Line 1, Address Line 2, City, State, ZIP, Country) can have its own validation rules:
- At least, At most (characters)
- Matches Regex Pattern
- Is, Is not
- Contains, Does not contain
### Contact Info Questions
Each contact field can have specific validation rules:
**First Name, Last Name, Company**:
- At least, At most (characters)
- Matches Regex Pattern, Is, Is not
- Contains, Does not contain
**Email**: Automatically validates email format, plus text rules above
**Phone**: Automatically validates phone format, plus text rules above
## Adding Validation Rules
<Steps>
<Step title="Open the Question Settings">
Click on the question you want to validate to open its settings panel.
</Step>
<Step title="Navigate to Validation Rules">
Scroll down to find the "Validation Rules" section and click to expand it.
</Step>
<Step title="Add a Rule">
Click the "Add rule" button to add a new validation rule.
</Step>
<Step title="Configure the Rule">
Select the rule type from the dropdown and enter the required value (if applicable).
</Step>
<Step title="Set Logic (Optional)">
If you have multiple rules, choose whether they should be combined with "All are true" or "Any is true" logic.
</Step>
<Step title="Save Your Survey">
Click "Save" to apply the validation rules to your survey.
</Step>
</Steps>
## Error Messages
Formbricks automatically generates user-friendly error messages based on your validation rules. Error messages are displayed below the input field when validation fails.
Example error messages:
- "Must be at least 10 characters"
- "Must be a valid email address"
- "Please select at least 2 options"
- "Date must be after 2025-01-01"
## Multi-Language Support
Validation rules work with multi-language surveys. Error messages are automatically displayed in the respondent's selected language.
## Combining Multiple Rules
When using multiple validation rules:
**All are true**: Use when all conditions must be met.
- Example: Text must be at least 10 characters AND contain "@email.com"
**Any is true**: Use when any condition is acceptable.
- Example: Date is earlier than 2025-01-01 OR is later than 2025-12-31

View File

@@ -91,11 +91,12 @@
"typeorm": ">=0.3.26",
"systeminformation": "5.27.14",
"qs": ">=6.14.1",
"preact": ">=10.26.10",
"fast-xml-parser": ">=5.3.4",
"diff": ">=8.0.3"
},
"comments": {
"overrides": "Security fixes for transitive dependencies. Remove when upstream packages update: axios (CVE-2025-58754) - awaiting @boxyhq/saml-jackson update | node-forge (Dependabot #230) - awaiting @boxyhq/saml-jackson update | tar-fs (Dependabot #205) - awaiting upstream dependency updates | typeorm (Dependabot #223) - awaiting @boxyhq/saml-jackson update | systeminformation (Dependabot #241) - awaiting @opentelemetry/host-metrics update | qs (Dependabot #245) - awaiting googleapis-common and stripe updates | fast-xml-parser (Dependabot #270) - awaiting @boxyhq/saml-jackson update | diff (Dependabot #269) - awaiting @microsoft/api-extractor update"
"overrides": "Security fixes for transitive dependencies. Remove when upstream packages update: axios (CVE-2025-58754) - awaiting @boxyhq/saml-jackson update | node-forge (Dependabot #230) - awaiting @boxyhq/saml-jackson update | tar-fs (Dependabot #205) - awaiting upstream dependency updates | typeorm (Dependabot #223) - awaiting @boxyhq/saml-jackson update | systeminformation (Dependabot #241) - awaiting @opentelemetry/host-metrics update | qs (Dependabot #245) - awaiting googleapis-common and stripe updates | preact (Dependabot #247) - awaiting next-auth update | fast-xml-parser (Dependabot #270) - awaiting @boxyhq/saml-jackson update | diff (Dependabot #269) - awaiting @microsoft/api-extractor update"
},
"patchedDependencies": {
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"

View File

@@ -62,7 +62,7 @@ export const removeEmptyImageAndVideoUrlsFromElements: MigrationScript = {
delete cleanedWelcomeCard.videoUrl;
}
const cleanedEndings = (survey.endings ?? []).map((ending) => {
const cleanedEndings = survey.endings.map((ending) => {
const cleanedEnding = { ...ending };
if (cleanedEnding.imageUrl === "") {
delete cleanedEnding.imageUrl;

View File

@@ -13,7 +13,7 @@ export function ProgressBar({ survey, blockId }: ProgressBarProps) {
[survey.blocks, blockId]
);
const endingCardIds = useMemo(() => (survey.endings ?? []).map((ending) => ending.id), [survey.endings]);
const endingCardIds = useMemo(() => survey.endings.map((ending) => ending.id), [survey.endings]);
const calculateProgress = useCallback(
(blockIndex: number) => {

View File

@@ -77,7 +77,7 @@ export function RenderSurvey(props: SurveyContainerProps) {
close();
}
},
(props.survey.endings?.length ?? 0) ? 3000 : 0 // close modal automatically after 3 seconds if no ending is enabled; otherwise, close immediately
props.survey.endings.length ? 3000 : 0 // close modal automatically after 3 seconds if no ending is enabled; otherwise, close immediately
);
}
}}

View File

@@ -422,7 +422,7 @@ export function Survey({
const evaluateLogicAndGetNextBlockId = (
data: TResponseData
): { nextBlockId: string | undefined; calculatedVariables: TResponseVariables } => {
const firstEndingId = (survey.endings?.length ?? 0) > 0 ? survey.endings[0].id : undefined;
const firstEndingId = survey.endings.length > 0 ? survey.endings[0].id : undefined;
if (blockId === "start")
return { nextBlockId: localSurvey.blocks[0]?.id || firstEndingId, calculatedVariables: {} };
@@ -657,7 +657,7 @@ export function Survey({
setIsSurveyFinished(finished);
const endingId = nextBlockId
? (localSurvey.endings ?? []).find((ending) => ending.id === nextBlockId)?.id
? localSurvey.endings.find((ending) => ending.id === nextBlockId)?.id
: undefined;
onChange(surveyResponseData);
@@ -776,7 +776,7 @@ export function Survey({
/>
);
} else if (blockIdx >= localSurvey.blocks.length) {
const endingCard = (localSurvey.endings ?? []).find((ending) => {
const endingCard = localSurvey.endings.find((ending) => {
return ending.id === blockId;
});
if (endingCard) {

View File

@@ -86,7 +86,7 @@ export function WelcomeCard({
const calculateTimeToComplete = () => {
const questions = getElementsFromSurveyBlocks(survey.blocks);
let totalCards = questions.length;
if ((survey.endings?.length ?? 0) > 0) totalCards += 1;
if (survey.endings.length > 0) totalCards += 1;
let idx = calculateElementIdx(survey, 0, totalCards);
if (idx === 0.5) {
idx = 1;

View File

@@ -164,7 +164,7 @@ export function StackedCardsContainer({
) : (
blockIdxTemp !== undefined &&
[prevBlockIdx, currentBlockIdx, nextBlockIdx, nextBlockIdx + 1].map((dynamicBlockIndex, index) => {
const hasEndingCard = (survey.endings?.length ?? 0) > 0;
const hasEndingCard = survey.endings.length > 0;
// Check for hiding extra card
if (dynamicBlockIndex > survey.blocks.length + (hasEndingCard ? 0 : -1)) return;
const offset = index - 1;

View File

@@ -87,7 +87,7 @@ export const calculateElementIdx = (
const currentQuestion = questions[currentQustionIdx];
const middleIdx = Math.floor(totalCards / 2);
const possibleNextBlockIds = getPossibleNextBlocks(survey.blocks, currentQuestion);
const endingCardIds = (survey.endings ?? []).map((ending) => ending.id);
const endingCardIds = survey.endings.map((ending) => ending.id);
// Convert block IDs to element IDs (get first element of each block)
const possibleNextQuestionIds = possibleNextBlockIds

160
pnpm-lock.yaml generated
View File

@@ -12,6 +12,7 @@ overrides:
typeorm: '>=0.3.26'
systeminformation: 5.27.14
qs: '>=6.14.1'
preact: '>=10.26.10'
fast-xml-parser: '>=5.3.4'
diff: '>=8.0.3'
@@ -986,7 +987,7 @@ importers:
specifier: 2.33.0
version: 2.33.0
preact:
specifier: 10.28.2
specifier: '>=10.26.10'
version: 10.28.2
react-i18next:
specifier: 15.7.3
@@ -3349,7 +3350,7 @@ packages:
'@prefresh/core@1.5.9':
resolution: {integrity: sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==}
peerDependencies:
preact: ^10.0.0 || ^11.0.0-0
preact: '>=10.26.10'
'@prefresh/utils@1.2.1':
resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==}
@@ -3357,7 +3358,7 @@ packages:
'@prefresh/vite@2.4.11':
resolution: {integrity: sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==}
peerDependencies:
preact: ^10.4.0 || ^11.0.0-0
preact: '>=10.26.10'
vite: '>=2.0.0'
'@prisma/client@6.14.0':
@@ -4540,8 +4541,8 @@ packages:
resolution: {integrity: sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==}
engines: {node: '>=18.0.0'}
'@smithy/core@3.22.0':
resolution: {integrity: sha512-6vjCHD6vaY8KubeNw2Fg3EK0KLGQYdldG4fYgQmA0xSW0dJ8G2xFhSOdrlUakWVoP5JuWHtFODg3PNd/DN3FDA==}
'@smithy/core@3.22.1':
resolution: {integrity: sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==}
engines: {node: '>=18.0.0'}
'@smithy/credential-provider-imds@4.2.7':
@@ -4628,16 +4629,16 @@ packages:
resolution: {integrity: sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-endpoint@4.4.12':
resolution: {integrity: sha512-9JMKHVJtW9RysTNjcBZQHDwB0p3iTP6B1IfQV4m+uCevkVd/VuLgwfqk5cnI4RHcp4cPwoIvxQqN4B1sxeHo8Q==}
'@smithy/middleware-endpoint@4.4.13':
resolution: {integrity: sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-retry@4.4.17':
resolution: {integrity: sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-retry@4.4.29':
resolution: {integrity: sha512-bmTn75a4tmKRkC5w61yYQLb3DmxNzB8qSVu9SbTYqW6GAL0WXO2bDZuMAn/GJSbOdHEdjZvWxe+9Kk015bw6Cg==}
'@smithy/middleware-retry@4.4.30':
resolution: {integrity: sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-serde@4.2.8':
@@ -4668,8 +4669,8 @@ packages:
resolution: {integrity: sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==}
engines: {node: '>=18.0.0'}
'@smithy/node-http-handler@4.4.8':
resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==}
'@smithy/node-http-handler@4.4.9':
resolution: {integrity: sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==}
engines: {node: '>=18.0.0'}
'@smithy/property-provider@4.2.7':
@@ -4732,8 +4733,8 @@ packages:
resolution: {integrity: sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==}
engines: {node: '>=18.0.0'}
'@smithy/smithy-client@4.11.1':
resolution: {integrity: sha512-SERgNg5Z1U+jfR6/2xPYjSEHY1t3pyTHC/Ma3YQl6qWtmiL42bvNId3W/oMUWIwu7ekL2FMPdqAmwbQegM7HeQ==}
'@smithy/smithy-client@4.11.2':
resolution: {integrity: sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==}
engines: {node: '>=18.0.0'}
'@smithy/types@4.11.0':
@@ -4780,16 +4781,16 @@ packages:
resolution: {integrity: sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==}
engines: {node: '>=18.0.0'}
'@smithy/util-defaults-mode-browser@4.3.28':
resolution: {integrity: sha512-/9zcatsCao9h6g18p/9vH9NIi5PSqhCkxQ/tb7pMgRFnqYp9XUOyOlGPDMHzr8n5ih6yYgwJEY2MLEobUgi47w==}
'@smithy/util-defaults-mode-browser@4.3.29':
resolution: {integrity: sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==}
engines: {node: '>=18.0.0'}
'@smithy/util-defaults-mode-node@4.2.19':
resolution: {integrity: sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==}
engines: {node: '>=18.0.0'}
'@smithy/util-defaults-mode-node@4.2.31':
resolution: {integrity: sha512-JTvoApUXA5kbpceI2vuqQzRjeTbLpx1eoa5R/YEZbTgtxvIB7AQZxFJ0SEyfCpgPCyVV9IT7we+ytSeIB3CyWA==}
'@smithy/util-defaults-mode-node@4.2.32':
resolution: {integrity: sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==}
engines: {node: '>=18.0.0'}
'@smithy/util-endpoints@3.2.7':
@@ -4820,8 +4821,8 @@ packages:
resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==}
engines: {node: '>=18.0.0'}
'@smithy/util-stream@4.5.10':
resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==}
'@smithy/util-stream@4.5.11':
resolution: {integrity: sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==}
engines: {node: '>=18.0.0'}
'@smithy/util-stream@4.5.8':
@@ -5278,7 +5279,7 @@ packages:
resolution: {integrity: sha512-F+kJ243LP6VmEK1M809unzTE/ijg+bsMNuiRN0JEDIJBELKKDNhdgC/WrUSZ7klwJvtlO3wQZ9ix+jhObG07Fg==}
engines: {node: '>= 12'}
peerDependencies:
preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0'
preact: '>=10.26.10'
'@testing-library/react@16.3.0':
resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==}
@@ -9329,10 +9330,7 @@ packages:
preact-render-to-string@5.2.6:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
preact: '>=10'
preact@10.26.6:
resolution: {integrity: sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==}
preact: '>=10.26.10'
preact@10.28.2:
resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==}
@@ -11454,7 +11452,7 @@ snapshots:
'@aws-sdk/util-user-agent-browser': 3.969.0
'@aws-sdk/util-user-agent-node': 3.971.0
'@smithy/config-resolver': 4.4.6
'@smithy/core': 3.22.0
'@smithy/core': 3.22.1
'@smithy/eventstream-serde-browser': 4.2.8
'@smithy/eventstream-serde-config-resolver': 4.3.8
'@smithy/eventstream-serde-node': 4.2.8
@@ -11465,25 +11463,25 @@ snapshots:
'@smithy/invalid-dependency': 4.2.8
'@smithy/md5-js': 4.2.8
'@smithy/middleware-content-length': 4.2.8
'@smithy/middleware-endpoint': 4.4.12
'@smithy/middleware-retry': 4.4.29
'@smithy/middleware-endpoint': 4.4.13
'@smithy/middleware-retry': 4.4.30
'@smithy/middleware-serde': 4.2.9
'@smithy/middleware-stack': 4.2.8
'@smithy/node-config-provider': 4.3.8
'@smithy/node-http-handler': 4.4.8
'@smithy/node-http-handler': 4.4.9
'@smithy/protocol-http': 5.3.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
'@smithy/url-parser': 4.2.8
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.3.28
'@smithy/util-defaults-mode-node': 4.2.31
'@smithy/util-defaults-mode-browser': 4.3.29
'@smithy/util-defaults-mode-node': 4.2.32
'@smithy/util-endpoints': 3.2.8
'@smithy/util-middleware': 4.2.8
'@smithy/util-retry': 4.2.8
'@smithy/util-stream': 4.5.10
'@smithy/util-stream': 4.5.11
'@smithy/util-utf8': 4.2.0
'@smithy/util-waiter': 4.2.8
tslib: 2.8.1
@@ -11636,26 +11634,26 @@ snapshots:
'@aws-sdk/util-user-agent-browser': 3.969.0
'@aws-sdk/util-user-agent-node': 3.971.0
'@smithy/config-resolver': 4.4.6
'@smithy/core': 3.22.0
'@smithy/core': 3.22.1
'@smithy/fetch-http-handler': 5.3.9
'@smithy/hash-node': 4.2.8
'@smithy/invalid-dependency': 4.2.8
'@smithy/middleware-content-length': 4.2.8
'@smithy/middleware-endpoint': 4.4.12
'@smithy/middleware-retry': 4.4.29
'@smithy/middleware-endpoint': 4.4.13
'@smithy/middleware-retry': 4.4.30
'@smithy/middleware-serde': 4.2.9
'@smithy/middleware-stack': 4.2.8
'@smithy/node-config-provider': 4.3.8
'@smithy/node-http-handler': 4.4.8
'@smithy/node-http-handler': 4.4.9
'@smithy/protocol-http': 5.3.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
'@smithy/url-parser': 4.2.8
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.3.28
'@smithy/util-defaults-mode-node': 4.2.31
'@smithy/util-defaults-mode-browser': 4.3.29
'@smithy/util-defaults-mode-node': 4.2.32
'@smithy/util-endpoints': 3.2.8
'@smithy/util-middleware': 4.2.8
'@smithy/util-retry': 4.2.8
@@ -11698,12 +11696,12 @@ snapshots:
dependencies:
'@aws-sdk/types': 3.969.0
'@aws-sdk/xml-builder': 3.969.0
'@smithy/core': 3.22.0
'@smithy/core': 3.22.1
'@smithy/node-config-provider': 4.3.8
'@smithy/property-provider': 4.2.8
'@smithy/protocol-http': 5.3.8
'@smithy/signature-v4': 5.3.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
'@smithy/util-base64': 4.3.0
'@smithy/util-middleware': 4.2.8
@@ -11780,12 +11778,12 @@ snapshots:
'@aws-sdk/core': 3.970.0
'@aws-sdk/types': 3.969.0
'@smithy/fetch-http-handler': 5.3.9
'@smithy/node-http-handler': 4.4.8
'@smithy/node-http-handler': 4.4.9
'@smithy/property-provider': 4.2.8
'@smithy/protocol-http': 5.3.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
'@smithy/util-stream': 4.5.10
'@smithy/util-stream': 4.5.11
tslib: 2.8.1
'@aws-sdk/credential-provider-ini@3.817.0':
@@ -12090,7 +12088,7 @@ snapshots:
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
'@smithy/util-middleware': 4.2.8
'@smithy/util-stream': 4.5.10
'@smithy/util-stream': 4.5.11
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
@@ -12184,15 +12182,15 @@ snapshots:
'@aws-sdk/core': 3.970.0
'@aws-sdk/types': 3.969.0
'@aws-sdk/util-arn-parser': 3.968.0
'@smithy/core': 3.22.0
'@smithy/core': 3.22.1
'@smithy/node-config-provider': 4.3.8
'@smithy/protocol-http': 5.3.8
'@smithy/signature-v4': 5.3.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
'@smithy/util-config-provider': 4.2.0
'@smithy/util-middleware': 4.2.8
'@smithy/util-stream': 4.5.10
'@smithy/util-stream': 4.5.11
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
@@ -12227,7 +12225,7 @@ snapshots:
'@aws-sdk/core': 3.970.0
'@aws-sdk/types': 3.969.0
'@aws-sdk/util-endpoints': 3.970.0
'@smithy/core': 3.22.0
'@smithy/core': 3.22.1
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
tslib: 2.8.1
@@ -12333,26 +12331,26 @@ snapshots:
'@aws-sdk/util-user-agent-browser': 3.969.0
'@aws-sdk/util-user-agent-node': 3.971.0
'@smithy/config-resolver': 4.4.6
'@smithy/core': 3.22.0
'@smithy/core': 3.22.1
'@smithy/fetch-http-handler': 5.3.9
'@smithy/hash-node': 4.2.8
'@smithy/invalid-dependency': 4.2.8
'@smithy/middleware-content-length': 4.2.8
'@smithy/middleware-endpoint': 4.4.12
'@smithy/middleware-retry': 4.4.29
'@smithy/middleware-endpoint': 4.4.13
'@smithy/middleware-retry': 4.4.30
'@smithy/middleware-serde': 4.2.9
'@smithy/middleware-stack': 4.2.8
'@smithy/node-config-provider': 4.3.8
'@smithy/node-http-handler': 4.4.8
'@smithy/node-http-handler': 4.4.9
'@smithy/protocol-http': 5.3.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
'@smithy/url-parser': 4.2.8
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.3.28
'@smithy/util-defaults-mode-node': 4.2.31
'@smithy/util-defaults-mode-browser': 4.3.29
'@smithy/util-defaults-mode-node': 4.2.32
'@smithy/util-endpoints': 3.2.8
'@smithy/util-middleware': 4.2.8
'@smithy/util-retry': 4.2.8
@@ -12391,7 +12389,7 @@ snapshots:
'@aws-sdk/client-s3': 3.971.0
'@aws-sdk/types': 3.969.0
'@aws-sdk/util-format-url': 3.969.0
'@smithy/middleware-endpoint': 4.4.12
'@smithy/middleware-endpoint': 4.4.13
'@smithy/signature-v4': 5.3.8
'@smithy/types': 4.12.0
'@smithy/util-hex-encoding': 4.2.0
@@ -12405,9 +12403,9 @@ snapshots:
'@aws-sdk/signature-v4-multi-region': 3.970.0
'@aws-sdk/types': 3.969.0
'@aws-sdk/util-format-url': 3.969.0
'@smithy/middleware-endpoint': 4.4.12
'@smithy/middleware-endpoint': 4.4.13
'@smithy/protocol-http': 5.3.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
tslib: 2.8.1
@@ -16007,7 +16005,7 @@ snapshots:
'@smithy/uuid': 1.1.0
tslib: 2.8.1
'@smithy/core@3.22.0':
'@smithy/core@3.22.1':
dependencies:
'@smithy/middleware-serde': 4.2.9
'@smithy/protocol-http': 5.3.8
@@ -16015,7 +16013,7 @@ snapshots:
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-middleware': 4.2.8
'@smithy/util-stream': 4.5.10
'@smithy/util-stream': 4.5.11
'@smithy/util-utf8': 4.2.0
'@smithy/uuid': 1.1.0
tslib: 2.8.1
@@ -16156,9 +16154,9 @@ snapshots:
'@smithy/util-middleware': 4.2.7
tslib: 2.8.1
'@smithy/middleware-endpoint@4.4.12':
'@smithy/middleware-endpoint@4.4.13':
dependencies:
'@smithy/core': 3.22.0
'@smithy/core': 3.22.1
'@smithy/middleware-serde': 4.2.9
'@smithy/node-config-provider': 4.3.8
'@smithy/shared-ini-file-loader': 4.4.3
@@ -16179,12 +16177,12 @@ snapshots:
'@smithy/uuid': 1.1.0
tslib: 2.8.1
'@smithy/middleware-retry@4.4.29':
'@smithy/middleware-retry@4.4.30':
dependencies:
'@smithy/node-config-provider': 4.3.8
'@smithy/protocol-http': 5.3.8
'@smithy/service-error-classification': 4.2.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
'@smithy/util-middleware': 4.2.8
'@smithy/util-retry': 4.2.8
@@ -16235,7 +16233,7 @@ snapshots:
'@smithy/types': 4.11.0
tslib: 2.8.1
'@smithy/node-http-handler@4.4.8':
'@smithy/node-http-handler@4.4.9':
dependencies:
'@smithy/abort-controller': 4.2.8
'@smithy/protocol-http': 5.3.8
@@ -16335,14 +16333,14 @@ snapshots:
'@smithy/util-stream': 4.5.8
tslib: 2.8.1
'@smithy/smithy-client@4.11.1':
'@smithy/smithy-client@4.11.2':
dependencies:
'@smithy/core': 3.22.0
'@smithy/middleware-endpoint': 4.4.12
'@smithy/core': 3.22.1
'@smithy/middleware-endpoint': 4.4.13
'@smithy/middleware-stack': 4.2.8
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
'@smithy/util-stream': 4.5.10
'@smithy/util-stream': 4.5.11
tslib: 2.8.1
'@smithy/types@4.11.0':
@@ -16400,10 +16398,10 @@ snapshots:
'@smithy/types': 4.11.0
tslib: 2.8.1
'@smithy/util-defaults-mode-browser@4.3.28':
'@smithy/util-defaults-mode-browser@4.3.29':
dependencies:
'@smithy/property-provider': 4.2.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
tslib: 2.8.1
@@ -16417,13 +16415,13 @@ snapshots:
'@smithy/types': 4.11.0
tslib: 2.8.1
'@smithy/util-defaults-mode-node@4.2.31':
'@smithy/util-defaults-mode-node@4.2.32':
dependencies:
'@smithy/config-resolver': 4.4.6
'@smithy/credential-provider-imds': 4.2.8
'@smithy/node-config-provider': 4.3.8
'@smithy/property-provider': 4.2.8
'@smithy/smithy-client': 4.11.1
'@smithy/smithy-client': 4.11.2
'@smithy/types': 4.12.0
tslib: 2.8.1
@@ -16465,10 +16463,10 @@ snapshots:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/util-stream@4.5.10':
'@smithy/util-stream@4.5.11':
dependencies:
'@smithy/fetch-http-handler': 5.3.9
'@smithy/node-http-handler': 4.4.8
'@smithy/node-http-handler': 4.4.9
'@smithy/types': 4.12.0
'@smithy/util-base64': 4.3.0
'@smithy/util-buffer-from': 4.2.0
@@ -20936,8 +20934,8 @@ snapshots:
next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.26.6
preact-render-to-string: 5.2.6(preact@10.26.6)
preact: 10.28.2
preact-render-to-string: 5.2.6(preact@10.28.2)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
uuid: 11.1.0
@@ -21487,13 +21485,11 @@ snapshots:
dependencies:
xtend: 4.0.2
preact-render-to-string@5.2.6(preact@10.26.6):
preact-render-to-string@5.2.6(preact@10.28.2):
dependencies:
preact: 10.26.6
preact: 10.28.2
pretty-format: 3.8.0
preact@10.26.6: {}
preact@10.28.2: {}
prebuild-install@7.1.3: